Grease Pencil: Boolean Carver Tool

This PR implements a new `Carver` tool in Draw Mode.

Both strokes & fills are cut along the lasso input. Holes are
automatically created when needed.

Notes:
* Cutting non-poly curves is currently not supported. All non-poly type
  curves near the lasso will be converted to poly, with a warning
  displayed to the user.
* Fills are automatically split into separate ids when the shape is
  split up into multiple fill islands.
* If a curve uses both stroke and fill, carving a hole into the fill
  will also add a stroke to the hole.
* If a curve uses both stroke and fill, carving into the shape from the
  outside will currently close the fill.

The implementation is based on a modified version of the Greiner-Hormann
clipping algorithm.

Pull Request: https://projects.blender.org/blender/blender/pulls/144181

---------

Co-authored-by: Falk David <falk@blender.org>
This commit is contained in:
Casey Bianco-Davis 2026-09-18 11:29:27 +02:00 • committed by Falk David
parent 20a730de80
commit 302eb8bddc
17 changed files with 3692 additions and 700 deletions

View file

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:b8ded96a8b53a9df98193a8a9e332dbb4fbd1d8590599c9db7159a439894232a
size 3986

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:3a74a6d883783b4b9b9ae920c504b6ac0b23a8921b906e1441d45a69575404a5
size 1916
oid sha256:e5a54e50e241616b3494e10615f139e8cc49e25795ecb2f536347f3c322b02a6
size 1214

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1bc4cb37ee64d304b4ebf0803a75872d218c05b4dca7442aeeb3b5cd667dca49
size 2870795
oid sha256:17678861c7798ce603f922c09ed564c6c8f0325b97e61b30e2f94ac569411843
size 2859369

View file

@ -8660,6 +8660,16 @@ def km_3d_view_tool_paint_grease_pencil_primitive_curve(_params):
)
def km_3d_view_tool_paint_grease_pencil_carver(params):
return (
"3D View Tool: Paint Grease Pencil, Carver",
{"space_type": 'VIEW_3D', "region_type": 'WINDOW'},
{"items": [
("grease_pencil.stroke_carver", {"type": params.tool_mouse, "value": 'PRESS'}, None),
]},
)
def km_3d_view_tool_paint_grease_pencil_eyedropper(params):
return (
"3D View Tool: Paint Grease Pencil, Eyedropper",
@ -9313,6 +9323,7 @@ def generate_keymaps(params=None):
km_3d_view_tool_edit_grease_pencil_texture_gradient(params),
km_3d_view_tool_edit_grease_pencil_pen(params),
km_3d_view_tool_edit_grease_pencil_interpolate(params),
km_3d_view_tool_paint_grease_pencil_carver(params),
*(km_sequencer_tool_generic_select_box(params, fallback=fallback)
for fallback in (False, True)),
*(km_sequencer_preview_tool_generic_select(params, fallback=fallback)

View file

@ -2372,6 +2372,25 @@ class _defs_grease_pencil_paint:
draw_settings=draw_settings,
)
@ToolDef.from_fn
def carver():
def draw_settings(context, layout, _tool):
brush = context.tool_settings.gpencil_paint.brush
gp_settings = brush.gpencil_settings
row = layout.row()
row.use_property_split = False
row.prop(gp_settings, "use_active_layer_only")
row.prop(gp_settings, "use_keep_caps_eraser")
return dict(
idname="builtin.carver",
label="Carver",
icon="ops.gpencil.carver",
cursor='KNIFE',
keymap=(),
draw_settings=draw_settings,
)
@staticmethod
def grease_pencil_primitive_toolbar(context, layout, _tool, props):
paint = context.tool_settings.gpencil_paint
@ -4192,6 +4211,7 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel):
*_tools_grease_pencil_primitives,
None,
_defs_grease_pencil_paint.trim,
_defs_grease_pencil_paint.carver,
None,
_defs_grease_pencil_paint.eyedropper,
None,

View file

@ -17,6 +17,34 @@ namespace blender::bke::greasepencil {
std::optional<FillCache> fill_cache_from_fill_ids(const VArray<int> &fill_ids);
struct ShapeData {
Vector<int> shape_map;
Vector<int> shape_offsets;
const GroupedSpan<int> shapes() const
{
return GroupedSpan<int>(shape_offsets.as_span(), shape_map.as_span());
};
};
/**
* Calculate all of the shapes from the "fill_id" attribute. Each shape is either a group of
* multiple curves that share the same "fill_id", or a single curve when the "fill_id" is zero.
*
* For example:
*
* curve index: 0 1 2 3 4 5 6 7 8
* fill_id: 0 0 a 0 a c a b b (a, b, c are some integers != 0)
*
* shape_map: 0 1 2 4 6 3 5 7 8
* shape_offsets: 0 1 2 5 6 7 9
* shapes: _ _ _____ _ _ ___
* a c b (ordered by the first occurrence in `fill_id`)
*
* Returns a #ShapeData struct with the #shape_map and #shape_offsets.
*/
ShapeData shapes_from_fill_ids(const VArray<int> &fill_ids, int curves_num);
/* Get the next available fill ID. */
int get_next_available_fill_id(Span<int> fill_ids);
int get_next_available_fill_id(const VArray<int> &fill_ids);

View file

@ -75,6 +75,98 @@ std::optional<FillCache> fill_cache_from_fill_ids(const VArray<int> &fill_ids)
return fill_cache;
}
ShapeData shapes_from_fill_ids(const VArray<int> &fill_ids, const int num_curves)
{
if (!fill_ids || fill_ids.is_empty()) {
Vector<int> shape_map(num_curves);
Vector<int> shape_offset(num_curves + 1);
array_utils::fill_index_range(shape_map.as_mutable_span());
array_utils::fill_index_range(shape_offset.as_mutable_span());
ShapeData shape_data;
shape_data.shape_map = std::move(shape_map);
shape_data.shape_offsets = std::move(shape_offset);
return shape_data;
}
BLI_assert(fill_ids.size() == num_curves);
/* The size of each shape. This includes zero fill id shapes (which always have a size of 1). */
Vector<int> shape_sizes;
/* Maps the non-zero fill id to the index of the shape. */
Map<int, int> fill_id_to_shape;
/* The fill id of each fill. The fill id zero can appear more than once, others may appear
* at most once. */
Vector<int> all_fill_ids;
/* The index of the curve if the shape is a zero fill. Otherwise -1. */
Vector<int> all_zero_fill_curve_indices;
/* Contains the curve indices for each non-zero fill. */
Vector<Vector<int>> curve_indices_by_non_zero_fill;
/* Maps the fill id to the index in the #curve_indices_by_non_zero_fill vector. */
Map<int, int> non_zero_fill_indexing;
for (const int curve : IndexRange(num_curves)) {
const int fill_id = fill_ids[curve];
if (fill_id == 0) {
shape_sizes.append(1);
all_fill_ids.append(0);
all_zero_fill_curve_indices.append(curve);
}
/* Try adding non zero fill id to the map. */
else if (fill_id_to_shape.add(fill_id, shape_sizes.size())) {
shape_sizes.append(1);
all_fill_ids.append(fill_id);
/* Not a zero fill. */
all_zero_fill_curve_indices.append(-1);
}
else {
shape_sizes[fill_id_to_shape.lookup(fill_id)]++;
}
/* Keep track of curve indices for non-zero fills. */
if (fill_id != 0) {
if (non_zero_fill_indexing.add(fill_id, curve_indices_by_non_zero_fill.size())) {
curve_indices_by_non_zero_fill.append(Vector<int>({curve}));
}
else {
curve_indices_by_non_zero_fill[non_zero_fill_indexing.lookup(fill_id)].append(curve);
}
}
}
shape_sizes.append(0);
OffsetIndices<int> shape_offsets = offset_indices::accumulate_counts_to_offsets(shape_sizes);
Vector<int> shape_map(num_curves);
MutableSpan<int> shape_map_span = shape_map.as_mutable_span();
threading::parallel_for(shape_offsets.index_range(), 4096, [&](const IndexRange range) {
for (const int shape_i : range) {
const IndexRange shape_range = shape_offsets[shape_i];
const bool is_zero_fill = all_fill_ids[shape_i] == 0;
if (is_zero_fill) {
const int curve_i = all_zero_fill_curve_indices[shape_i];
BLI_assert(shape_range.size() == 1);
shape_map_span[shape_range.first()] = curve_i;
}
else {
const int fill_id = all_fill_ids[shape_i];
const Span<int> curve_indices =
curve_indices_by_non_zero_fill[non_zero_fill_indexing.lookup(fill_id)].as_span();
shape_map_span.slice(shape_range).copy_from(curve_indices);
}
}
});
ShapeData shape_data;
shape_data.shape_map = std::move(shape_map);
shape_data.shape_offsets = std::move(shape_sizes);
return shape_data;
}
static int get_next_available_fill_id_from_max(const int max_fill_id)
{
/* Make sure the fill ID is greater than zero. This avoids the issue of hitting an invalid fill

View file

@ -75,6 +75,7 @@ set_property(GLOBAL PROPERTY ICON_GEOM_NAMES
ops.generic.select_circle
ops.generic.select_lasso
ops.generic.select_paint
ops.gpencil.carver
ops.gpencil.draw
ops.gpencil.draw.eraser
ops.gpencil.draw.line

View file

@ -19,6 +19,8 @@ set(INC_SYS
set(SRC
intern/grease_pencil_add.cc
intern/grease_pencil_bake_animation.cc
intern/grease_pencil_boolean.cc
intern/grease_pencil_carver.cc
intern/grease_pencil_edit.cc
intern/grease_pencil_frames.cc
intern/grease_pencil_geom.cc
@ -40,6 +42,8 @@ set(SRC
intern/grease_pencil_utils.cc
intern/grease_pencil_vertex_paint.cc
intern/grease_pencil_weight_paint.cc
intern/grease_pencil_segments_intern.hh
)
set(LIB
@ -67,6 +71,7 @@ if(WITH_GTESTS)
set(TEST_SRC
tests/grease_pencil_merge_test.cc
tests/grease_pencil_trim_test.cc
tests/grease_pencil_boolean_test.cc
)
set(TEST_INC
)

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,347 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edgreasepencil
*/
#include "BKE_brush.hh"
#include "BKE_context.hh"
#include "BKE_crazyspace.hh"
#include "BKE_grease_pencil_fills.hh"
#include "BKE_material.hh"
#include "BKE_paint.hh"
#include "DEG_depsgraph_query.hh"
#include "ED_curves.hh"
#include "ED_grease_pencil.hh"
#include "ED_view3d.hh"
#include "BLI_bounds.hh"
#include "DNA_brush_types.h"
#include "DNA_material_types.h"
#include "DNA_windowmanager_types.h"
#include "WM_api.hh"
#include "GEO_resample_curves.hh"
#include "GEO_smooth_curves.hh"
namespace blender {
namespace ed::greasepencil {
/**
* Apply the stroke carver to a drawing.
*/
static bool execute_carver_on_drawing(const Object &ob_eval,
Object &obact,
const ARegion &region,
const float4x4 &projection,
const float4x4 &layer_to_world,
const DrawingPlacement &placement,
const Span<float2> mcoords,
const bool keep_caps,
bke::greasepencil::Drawing &drawing,
bool *r_converted)
{
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, drawing);
const Span<float3> normals = drawing.curve_plane_normals();
Array<float4> curve_planes(src.curves_num());
threading::parallel_for(src.curves_range(), 4096, [&](const IndexRange src_curves) {
for (const int src_curve : src_curves) {
const float3 &normal = normals[src_curve];
const IndexRange points = src_points_by_curve[src_curve];
const float3 &point = deformation.positions[points.first()];
curve_planes[src_curve] = float4(normal, -math::dot(point, normal));
}
});
bke::CurvesGeometry input_curves = bke::CurvesGeometry(src);
input_curves.resize(src.points_num() + mcoords.size(), src.curves_num() + 1);
input_curves.offsets_for_write().last() = src.points_num() + mcoords.size();
bke::MutableAttributeAccessor attributes = input_curves.attributes_for_write();
placement.project(mcoords, input_curves.positions_for_write().take_back(mcoords.size()));
bke::SpanAttributeWriter<int> fill_ids = attributes.lookup_or_add_for_write_span<int>(
"fill_id", bke::AttrDomain::Curve);
fill_ids.span.last() = bke::greasepencil::get_next_available_fill_id(fill_ids.span.varray());
const bke::greasepencil::ShapeData shapes_data = bke::greasepencil::shapes_from_fill_ids(
fill_ids.span.varray(), input_curves.curves_num());
fill_ids.finish();
const IndexRange clipping_points = IndexRange::from_begin_size(src.points_num(), mcoords.size());
const IndexRange clipping_curves = IndexRange::from_single(src.curves_num());
input_curves.fill_curve_types(clipping_curves, CURVE_TYPE_POLY);
input_curves.cyclic_for_write().last() = true;
/* Initialize the rest of the attributes with default values. */
bke::fill_attribute_range_default(attributes,
bke::AttrDomain::Point,
bke::attribute_filter_from_skip_ref({"position"}),
clipping_points);
bke::fill_attribute_range_default(
attributes,
bke::AttrDomain::Curve,
bke::attribute_filter_from_skip_ref({"fill_id", "cyclic", "curve_type"}),
clipping_curves);
auto project_fn = [&](const float3 &position) {
return ED_view3d_project_float_v2_m4(&region, position, projection);
};
/* WORKAROUND. Currently only poly curves are supported. Convert any curve close to the clipping
* shape to poly. */
if (!input_curves.is_single_type(CURVE_TYPE_POLY)) {
const OffsetIndices<int> points_by_curve = input_curves.points_by_curve();
Array<float2> src_positions_2d(input_curves.points_num());
const Span<float3> positions = input_curves.positions();
for (const int i : input_curves.points_range()) {
src_positions_2d[i] = project_fn(positions[i]);
}
Array<Bounds<float2>> screen_space_bbox(input_curves.curves_num());
threading::parallel_for(points_by_curve.index_range(), 512, [&](const IndexRange curves_i) {
for (const int curve_i : curves_i) {
const IndexRange points_i = points_by_curve[curve_i];
screen_space_bbox[curve_i] = *bounds::min_max(src_positions_2d.as_span().slice(points_i));
}
});
const VArray<int8_t> curve_types = input_curves.curve_types();
const Bounds<float2> bbox_j = screen_space_bbox.last();
IndexMaskMemory memory;
IndexMask curves_to_convert = IndexMask::from_predicate(
input_curves.curves_range(), memory, [&](const int64_t curve_i) {
const Bounds<float2> bbox_i = screen_space_bbox[curve_i];
/* Poly curve do not need to be converted. */
if (curve_types[curve_i] == CURVE_TYPE_POLY) {
return false;
}
if (bounds::intersect(bbox_i, bbox_j).has_value()) {
return true;
}
return false;
});
curves_to_convert = bke::greasepencil::selected_mask_to_fills(
curves_to_convert, input_curves, bke::AttrDomain::Curve, memory);
if (!curves_to_convert.is_empty()) {
input_curves = geometry::resample_to_evaluated(input_curves, curves_to_convert);
*r_converted = true;
}
}
boolean::CurveBooleanOpParameters op_params;
op_params.boolean_mode = boolean::Operation::Difference;
op_params.keep_caps = keep_caps;
op_params.skip_clipping_attributes = true;
op_params.separate_islands = true;
const GroupedSpan<int> shapes = shapes_data.shapes();
const IndexRange clipping_shapes = IndexRange::from_single(shapes.size() - 1);
bke::CurvesGeometry carved_strokes = boolean::curve_boolean_with_planes(op_params,
input_curves,
project_fn,
shapes,
curve_planes,
clipping_shapes,
layer_to_world,
region);
/* Set the new geometry. */
drawing.strokes_for_write() = std::move(carved_strokes);
drawing.tag_topology_changed();
return true;
}
/**
* Apply the stroke carver to all layers.
*/
static wmOperatorStatus grease_pencil_stroke_carver_exec(bContext *C, wmOperator *op)
{
const Array<int2> mcoords = WM_gesture_lasso_path_to_array(C, op);
if (mcoords.is_empty()) {
return OPERATOR_PASS_THROUGH;
}
const Scene *scene = CTX_data_scene(C);
const ARegion *region = CTX_wm_region(C);
const RegionView3D *rv3d = CTX_wm_region_view3d(C);
const Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
View3D *view3d = CTX_wm_view3d(C);
Object *obact = CTX_data_active_object(C);
Object *ob_eval = DEG_get_evaluated(depsgraph, obact);
GreasePencil &grease_pencil = *id_cast<GreasePencil *>(obact->data);
Array<float2> coords(mcoords.size());
threading::parallel_for(mcoords.index_range(), 4096, [&](const IndexRange i_range) {
for (const int i : i_range) {
coords[i] = float2(mcoords[i]);
}
});
Array<float2> lasso_pos(coords.size());
const int smooth_iterations = 4;
const float smooth_factor = 0.8f;
geometry::gaussian_blur_1D(coords.as_span(),
smooth_iterations,
VArray<float>::from_single(smooth_factor, coords.size()),
true,
true,
false,
lasso_pos.as_mutable_span());
Paint *paint = BKE_paint_get_active_from_context(C);
Brush *brush = BKE_paint_brush(paint);
if (brush->gpencil_settings == nullptr) {
BKE_brush_init_gpencil_settings(brush);
}
const bool keep_caps = (brush->gpencil_settings->flag & GP_BRUSH_ERASER_KEEP_CAPS) != 0;
const bool active_layer_only = (brush->gpencil_settings->flag & GP_BRUSH_ACTIVE_LAYER_ONLY) != 0;
std::atomic<bool> changed = false;
std::atomic<bool> converted = false;
bool inserted_keyframe = false;
if (active_layer_only) {
/* Apply carver on drawings of active layer. */
if (!grease_pencil.has_active_layer()) {
return OPERATOR_CANCELLED;
}
bke::greasepencil::Layer &layer = *grease_pencil.get_active_layer();
if (!layer.is_editable()) {
return OPERATOR_CANCELLED;
}
ensure_active_keyframe(*scene, grease_pencil, layer, true, inserted_keyframe);
const float4x4 layer_to_world = layer.to_world_space(*ob_eval);
const float4x4 projection = ED_view3d_ob_project_mat_get_from_obmat(rv3d, layer_to_world);
const Vector<ed::greasepencil::MutableDrawingInfo> drawings =
ed::greasepencil::retrieve_editable_drawings_from_layer(*scene, grease_pencil, layer);
/* Initialize helper class for projecting screen space coordinates. */
DrawingPlacement placement = ed::greasepencil::DrawingPlacement(
*scene, *region, *view3d, *ob_eval, &layer);
threading::parallel_for_each(drawings, [&](const ed::greasepencil::MutableDrawingInfo &info) {
bool r_converted = false;
if (execute_carver_on_drawing(*ob_eval,
*obact,
*region,
projection,
layer_to_world,
placement,
lasso_pos.as_span(),
keep_caps,
info.drawing,
&r_converted))
{
changed = true;
}
if (r_converted) {
converted = true;
}
});
}
else {
for (bke::greasepencil::Layer *layer : grease_pencil.layers_for_write()) {
if (layer->is_editable()) {
ed::greasepencil::ensure_active_keyframe(
*scene, grease_pencil, *layer, true, inserted_keyframe);
}
}
/* Apply carver on every editable drawing. */
const Vector<ed::greasepencil::MutableDrawingInfo> drawings =
ed::greasepencil::retrieve_editable_drawings(*scene, grease_pencil);
threading::parallel_for_each(drawings, [&](const ed::greasepencil::MutableDrawingInfo &info) {
const bke::greasepencil::Layer &layer = grease_pencil.layer(info.layer_index);
const float4x4 layer_to_world = layer.to_world_space(*ob_eval);
const float4x4 projection = ED_view3d_ob_project_mat_get_from_obmat(rv3d, layer_to_world);
/* Initialize helper class for projecting screen space coordinates. */
DrawingPlacement placement = ed::greasepencil::DrawingPlacement(
*scene, *region, *view3d, *ob_eval, &layer);
bool r_converted = false;
if (execute_carver_on_drawing(*ob_eval,
*obact,
*region,
projection,
layer_to_world,
placement,
lasso_pos,
keep_caps,
info.drawing,
&r_converted))
{
changed = true;
}
if (r_converted) {
converted = true;
}
});
}
if (converted) {
BKE_report(op->reports, RPT_WARNING, "Some curves were converted to Poly");
}
if (changed) {
DEG_id_tag_update(&grease_pencil.id, ID_RECALC_GEOMETRY);
WM_event_add_notifier(C, NC_GEOM | ND_DATA, &grease_pencil);
if (inserted_keyframe) {
WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, nullptr);
}
}
return OPERATOR_FINISHED;
}
} // namespace ed::greasepencil
void GREASE_PENCIL_OT_stroke_carver(wmOperatorType *ot)
{
using namespace ed::greasepencil;
ot->name = "Grease Pencil Carver";
ot->idname = "GREASE_PENCIL_OT_stroke_carver";
ot->description = "Cuts stroke point in the intersect lasso";
ot->invoke = WM_gesture_lasso_invoke;
ot->modal = WM_gesture_lasso_modal;
ot->exec = grease_pencil_stroke_carver_exec;
ot->poll = grease_pencil_painting_poll;
ot->cancel = WM_gesture_lasso_cancel;
ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
WM_operator_properties_gesture_lasso(ot);
}
} // namespace blender

View file

@ -5552,6 +5552,7 @@ void ED_operatortypes_grease_pencil_edit()
WM_operatortype_append(GREASE_PENCIL_OT_set_stroke_type);
WM_operatortype_append(GREASE_PENCIL_OT_join_fills);
WM_operatortype_append(GREASE_PENCIL_OT_separate_fills);
WM_operatortype_append(GREASE_PENCIL_OT_stroke_carver);
}
/* -------------------------------------------------------------------- */

View file

@ -0,0 +1,197 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup editors
*/
#pragma once
#include "BKE_grease_pencil.hh"
namespace blender::ed::greasepencil::segment {
/* 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;
enum Side : uint8_t { Start = 0, End = 1 };
/**
* Structure describing a curve segment (a point range in a curve) with end intersection points.
* A Segment can go past the end of the source curve and loop back to the start.
*/
class Segment {
public:
/* Curve index. */
int curve = -1;
/* The start and end of the original curve is stored, because this segment may go past the end
* and have to loop. */
IndexRange src_points;
/* Point range of the segment: Starting point and end point. Matches the point offsets
* in a CurvesGeometry. */
int points[2] = {-1, -1};
/* If this segment is a full cyclical segment, note that the segment can start and end at some
* intersection point. */
bool full_wrap_loop = false;
/* The normalized distance where the trim segment is intersected by another curve.
* For the outer ends of the trim segment the intersection distance is given between:
* - [start point] and [start point + 1]
* - [end point] and [end point + 1]
*/
float intersection_factor[2] = {0.0f, 0.0f};
int intersection_index[2] = {-1, -1};
constexpr Segment() = default;
bool is_loop() const;
bool has_intersection(Side side) const;
int2 edge(Side side) const;
int wrap_index(int i) const;
IndexRange point_range() const;
int points_num() const;
template<typename Fn> void foreach_point(Fn &&fn) const;
static Segment from_curve(int curve_i, IndexRange points, bool cyclic);
static Segment from_intersections(int curve_i,
IndexRange points,
const std::optional<int> point_start,
const std::optional<int> point_end,
const std::optional<float> factor_start,
const std::optional<float> factor_end,
const std::optional<int> inter_index_start,
const std::optional<int> inter_index_end);
};
template<typename Fn> void Segment::foreach_point(Fn &&fn) const
{
const IndexRange point_range = this->point_range();
for (const int64_t pos : point_range.index_range()) {
const int i = this->wrap_index(point_range[pos]);
if constexpr (std::is_invocable_r_v<void, Fn, int64_t, int64_t>) {
fn(i, pos);
}
else {
fn(i);
}
}
}
struct IntersectionPoint {
int point_i = -1;
int point_j = -1;
float factor_i = -1.0f;
float factor_j = -1.0f;
int curve_i = -1;
int curve_j = -1;
int segment_index_i[2] = {-1, -1};
int segment_index_j[2] = {-1, -1};
constexpr IntersectionPoint() = default;
float point_for_curve(const int curve) const
{
BLI_assert(curve == curve_i || curve == curve_j);
return curve == curve_i ? point_i : point_j;
}
float factor_for_curve(const int curve) const
{
BLI_assert(curve == curve_i || curve == curve_j);
return curve == curve_i ? factor_i : factor_j;
}
float parameter_for_curve(const int curve) const
{
return this->point_for_curve(curve) + this->factor_for_curve(curve);
}
int other_curve(const int curve) const
{
BLI_assert(curve == curve_i || curve == curve_j);
return curve == curve_i ? curve_j : curve_i;
}
};
IntersectionPoint create_intersection(const int point_i,
const int point_j,
const float factor_i,
const float factor_j,
const int curve_i,
const int curve_j);
using EncodedConnection = int;
constexpr EncodedConnection SEGMENT_CONNECTION_NULL = 0;
/* We store the side as sign, but because a segment with index zero is valid, we shift by one. */
EncodedConnection encode_index_and_side(const int index, const Side side);
int decode_index(const EncodedConnection encoded);
Side decode_side(const EncodedConnection encoded);
/* Both the start and end of every segment is connected to two other segments or null. */
using SegmentConnections = VecBase<EncodedConnection, 2>;
/* Compute bounding boxes of curves in screen space. The bounding boxes are used to speed
* up the search for intersecting curves. */
void compute_bounding_boxes(const OffsetIndices<int> src_points_by_curve,
const Span<float2> screen_space_positions,
MutableSpan<Bounds<float2>> screen_space_bbox);
void compute_bounding_boxes(const OffsetIndices<int> src_points_by_curve,
const GroupedSpan<int> shapes,
const Span<float2> screen_space_positions,
MutableSpan<Bounds<float2>> screen_space_bbox);
void cut_caps(bke::CurvesGeometry &dst,
const Span<Segment> segments,
const Span<bool> segment_reversed,
const Span<bool> cyclic,
const OffsetIndices<int> segment_offsets);
void find_intersections_between_all_curves(const Span<float2> screen_space_positions,
const Span<Bounds<float2>> screen_space_bbox,
const OffsetIndices<int> points_by_curve,
const VArray<bool> &cyclic,
const IndexMask &visible_curves,
Array<Vector<int>> &r_inters_per_curves,
Vector<IntersectionPoint> &r_intersections);
void store_segment_map_on_intersections(const Span<Segment> all_segments,
MutableSpan<IntersectionPoint> intersections);
int create_segments_from_intersections_single_curve(const int curve_k,
const Span<Vector<int>> inters_per_curves,
const OffsetIndices<int> points_by_curve,
const Span<IntersectionPoint> &intersections,
const VArray<bool> &cyclic,
Vector<Segment> &all_segments);
void create_segments_from_intersections(const Span<Vector<int>> inters_per_curves,
const OffsetIndices<int> points_by_curve,
const Span<IntersectionPoint> &intersections,
const VArray<bool> &cyclic,
Vector<Segment> &all_segments,
MutableSpan<int> segments_num_per_curve);
void create_connections_from_curves(const OffsetIndices<int> segments_by_curve,
const Span<bool> segments_to_keep,
const VArray<bool> &is_cyclic,
MutableSpan<SegmentConnections> segment_connections);
void follow_segment_connections(const Span<Segment> all_segments,
const Span<bool> segments_to_keep,
const Span<SegmentConnections> segment_connections,
Vector<Segment> &segments,
Vector<int> &segment_offset_data,
Vector<bool> &segment_reversed,
Vector<bool> &cyclic);
} // namespace blender::ed::greasepencil::segment

View file

@ -7,6 +7,7 @@
*/
#include "BLI_array.hh"
#include "BLI_bounds.hh"
#include "BLI_lasso_2d.hh"
#include "BLI_rect.hh"
#include "BLI_task.hh"
@ -28,10 +29,403 @@
#include "WM_api.hh"
#include "grease_pencil_segments_intern.hh"
namespace blender {
namespace ed::greasepencil {
namespace trim {
using namespace segment;
static bool check_line_segment_lasso_intersection(const int2 &pos_a,
const int2 &pos_b,
const Span<int2> mcoords)
{
Bounds<int2> bbox_ab{math::min(pos_a, pos_b), math::max(pos_a, pos_b)};
bbox_ab.pad(BBOX_PADDING);
/* Check the lasso bounding box first as an optimization. */
if (bbox_ab.intersects_segment(pos_a, pos_b) &&
BLI_lasso_is_edge_inside(mcoords, pos_a.x, pos_a.y, pos_b.x, pos_b.y, IS_CLIPPED))
{
return true;
}
return false;
}
static void check_segments_in_lasso(const Span<float2> screen_space_positions,
const Span<Bounds<float2>> screen_space_bbox,
const Span<int2> mcoords,
const Span<Segment> all_segments,
const IndexMask &editable_curves,
const OffsetIndices<int> segments_by_curve,
MutableSpan<bool> segments_to_keep)
{
const Bounds<int2> bbox_lasso_int = *bounds::min_max(mcoords);
const Bounds<float2> bbox_lasso{float2(bbox_lasso_int.min), float2(bbox_lasso_int.max)};
editable_curves.foreach_index(
[&](const int curve_i) {
/* To speed things up: Do a bounding box check on the curve and the lasso area. */
if (!bounds::intersect(bbox_lasso, screen_space_bbox[curve_i]).has_value()) {
return;
}
const IndexRange &segment_range = segments_by_curve[curve_i];
for (const int segment_i : segment_range) {
const Segment &segment = all_segments[segment_i];
const IndexRange point_range = segment.point_range();
if (point_range.is_empty()) {
const float start_factor = segment.intersection_factor[Side::Start];
const int2 start_edge = segment.edge(Side::Start);
const float end_factor = segment.intersection_factor[Side::End];
const int2 end_edge = segment.edge(Side::End);
const float2 pos_1 = math::interpolate(screen_space_positions[start_edge.x],
screen_space_positions[start_edge.y],
start_factor);
const float2 pos_2 = math::interpolate(screen_space_positions[end_edge.x],
screen_space_positions[end_edge.y],
end_factor);
if (check_line_segment_lasso_intersection(int2(pos_1), int2(pos_2), mcoords)) {
segments_to_keep[segment_i] = false;
}
continue;
}
for (const int64_t i : point_range.drop_back(1)) {
const int point_i1 = segment.wrap_index(i);
const int point_i2 = segment.wrap_index(i + 1);
const float2 pos_1 = screen_space_positions[point_i1];
const float2 pos_2 = screen_space_positions[point_i2];
if (check_line_segment_lasso_intersection(int2(pos_1), int2(pos_2), mcoords)) {
segments_to_keep[segment_i] = false;
continue;
}
}
if (segment_range.size() == 1 && segment.is_loop()) {
const float2 pos_1 = screen_space_positions[segment.wrap_index(point_range.first())];
const float2 pos_2 = screen_space_positions[segment.wrap_index(point_range.last())];
if (check_line_segment_lasso_intersection(int2(pos_1), int2(pos_2), mcoords)) {
segments_to_keep[segment_i] = false;
continue;
}
}
else {
if (segment.has_intersection(Side::Start)) {
const float start_factor = segment.intersection_factor[Side::Start];
const int2 start_edge = segment.edge(Side::Start);
const float2 pos_1 = math::interpolate(screen_space_positions[start_edge.x],
screen_space_positions[start_edge.y],
start_factor);
const float2 pos_2 = screen_space_positions[segment.wrap_index(point_range.first())];
if (check_line_segment_lasso_intersection(int2(pos_1), int2(pos_2), mcoords)) {
segments_to_keep[segment_i] = false;
continue;
}
}
if (segment.has_intersection(Side::End)) {
const float end_factor = segment.intersection_factor[Side::End];
const int2 end_edge = segment.edge(Side::End);
const float2 pos_1 = screen_space_positions[segment.wrap_index(point_range.last())];
const float2 pos_2 = math::interpolate(screen_space_positions[end_edge.x],
screen_space_positions[end_edge.y],
end_factor);
if (check_line_segment_lasso_intersection(int2(pos_1), int2(pos_2), mcoords)) {
segments_to_keep[segment_i] = false;
continue;
}
}
}
}
},
exec_mode::grain_size(128));
}
static bke::CurvesGeometry create_curves_from_segments(const bke::CurvesGeometry &src,
const Span<Segment> segments,
const Span<bool> segment_reversed,
const Span<bool> cyclic,
const OffsetIndices<int> segment_offsets)
{
struct InterpolatePoint {
int src_point_1;
int src_point_2;
float factor;
};
Array<int> point_offsets(segment_offsets.size() + 1);
Vector<InterpolatePoint> point_to_interpolate;
for (const int curve_i : segment_offsets.index_range()) {
point_offsets[curve_i] = point_to_interpolate.size();
const IndexRange segment_range = segment_offsets[curve_i];
for (const int seg_i : segment_range) {
const Segment &segment = segments[seg_i];
const bool reversed = segment_reversed[seg_i];
const Side start_side = reversed ? Side::End : Side::Start;
const Side end_side = reversed ? Side::Start : Side::End;
if (segment.has_intersection(start_side) && !segment.is_loop()) {
const float start_factor = segment.intersection_factor[start_side];
const int2 start_edge = segment.edge(start_side);
point_to_interpolate.append({start_edge.x, start_edge.y, start_factor});
}
segment.foreach_point(
[&](const int index) { point_to_interpolate.append({index, index, 0.0f}); });
if (reversed) {
point_to_interpolate.as_mutable_span().take_back(segment.points_num()).reverse();
}
if (seg_i == segment_range.last() && segment.has_intersection(end_side) && !cyclic[curve_i])
{
const float end_factor = segment.intersection_factor[end_side];
const int2 end_edge = segment.edge(end_side);
point_to_interpolate.append({end_edge.x, end_edge.y, end_factor});
}
}
}
point_offsets.last() = point_to_interpolate.size();
const OffsetIndices<int> dst_points_by_curve = OffsetIndices<int>(point_offsets);
if (dst_points_by_curve.total_size() == 0) {
return bke::CurvesGeometry();
}
bke::CurvesGeometry dst_curves(dst_points_by_curve.total_size(), dst_points_by_curve.size());
bke::MutableAttributeAccessor dst_attributes = dst_curves.attributes_for_write();
dst_curves.offsets_for_write().copy_from(dst_points_by_curve.data());
dst_curves.cyclic_for_write().copy_from(cyclic);
Array<int> old_by_new_map(dst_points_by_curve.size());
threading::parallel_for(dst_points_by_curve.index_range(), 4096, [&](const IndexRange points) {
for (const int i : points) {
const IndexRange segment_range = segment_offsets[i];
old_by_new_map[i] = segments[segment_range.first()].curve;
}
});
const bke::AttributeAccessor src_attributes = src.attributes();
bke::gather_attributes(src_attributes,
bke::AttrDomain::Curve,
bke::AttrDomain::Curve,
bke::attribute_filter_from_skip_ref({"cyclic"}),
old_by_new_map,
dst_attributes);
/* Copy/Interpolate point attributes. */
for (auto &attribute : bke::retrieve_attributes_for_transfer(
src_attributes, dst_attributes, {bke::AttrDomain::Point}, {}))
{
bke::attribute_math::to_static_type(attribute.dst.span.type(), [&]<typename T>() {
if constexpr (!std::is_same_v<T, std::string>) {
const Span<T> src_attr = attribute.src.typed<T>();
MutableSpan<T> dst_attr = attribute.dst.span.typed<T>();
threading::parallel_for(
point_to_interpolate.index_range(), 4096, [&](const IndexRange points) {
for (const int i : points) {
const InterpolatePoint &int_point = point_to_interpolate[i];
if (int_point.factor == 0.0f) {
dst_attr[i] = src_attr[int_point.src_point_1];
}
else if (int_point.factor == 1.0f) {
dst_attr[i] = src_attr[int_point.src_point_2];
}
else {
dst_attr[i] = bke::attribute_math::mix2<T>(int_point.factor,
src_attr[int_point.src_point_1],
src_attr[int_point.src_point_2]);
}
}
});
}
});
attribute.dst.finish();
}
return dst_curves;
}
bke::CurvesGeometry trim_curve_segments(const bke::CurvesGeometry &src,
const Span<float2> screen_space_positions,
const Span<int2> mcoords,
const IndexMask &editable_curves,
const IndexMask &visible_curves,
const bool keep_caps)
{
if (src.is_empty()) {
return src;
}
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
const VArray<bool> is_cyclic = src.cyclic();
Array<Bounds<float2>> screen_space_bbox(src.curves_num());
compute_bounding_boxes(src_points_by_curve, screen_space_positions, screen_space_bbox);
Vector<IntersectionPoint> intersections;
Array<int> all_segment_offset_data(src_points_by_curve.size() + 1);
Vector<Segment> all_segments;
Array<Vector<int>> inters_per_curves(src_points_by_curve.size());
find_intersections_between_all_curves(screen_space_positions,
screen_space_bbox,
src_points_by_curve,
is_cyclic,
visible_curves,
inters_per_curves,
intersections);
create_segments_from_intersections(inters_per_curves,
src_points_by_curve,
intersections,
is_cyclic,
all_segments,
all_segment_offset_data.as_mutable_span().drop_back(1));
store_segment_map_on_intersections(all_segments, intersections);
const OffsetIndices<int> segments_by_curve = offset_indices::accumulate_counts_to_offsets(
all_segment_offset_data);
Array<bool> segments_to_keep(all_segments.size(), true);
check_segments_in_lasso(screen_space_positions,
screen_space_bbox,
mcoords,
all_segments,
editable_curves,
segments_by_curve,
segments_to_keep.as_mutable_span());
Array<SegmentConnections> segment_connections(all_segments.size(),
SegmentConnections(SEGMENT_CONNECTION_NULL));
create_connections_from_curves(
segments_by_curve, segments_to_keep, is_cyclic, segment_connections.as_mutable_span());
Vector<Segment> segments;
Vector<int> segment_offset_data;
Vector<bool> segment_reversed;
Vector<bool> cyclic;
follow_segment_connections(all_segments,
segments_to_keep,
segment_connections,
segments,
segment_offset_data,
segment_reversed,
cyclic);
const OffsetIndices<int> segment_offsets = OffsetIndices<int>(segment_offset_data);
bke::CurvesGeometry dst = create_curves_from_segments(
src, segments, segment_reversed, cyclic, segment_offsets);
if (!keep_caps) {
cut_caps(dst, segments, segment_reversed, cyclic, segment_offsets);
}
return dst;
}
bke::CurvesGeometry trim_curve_segment_ends(const bke::CurvesGeometry &src,
const Span<float2> screen_space_positions,
const IndexMask &editable_curves,
const IndexMask &visible_curves,
const bool keep_caps)
{
if (src.is_empty()) {
return src;
}
const OffsetIndices<int> src_points_by_curve = src.points_by_curve();
const VArray<bool> is_cyclic = src.cyclic();
Array<Bounds<float2>> screen_space_bbox(src.curves_num());
compute_bounding_boxes(src_points_by_curve, screen_space_positions, screen_space_bbox);
Vector<IntersectionPoint> intersections;
Array<int> all_segment_offset_data(src_points_by_curve.size() + 1);
Vector<Segment> all_segments;
Array<Vector<int>> inters_per_curves(src_points_by_curve.size());
find_intersections_between_all_curves(screen_space_positions,
screen_space_bbox,
src_points_by_curve,
is_cyclic,
visible_curves,
inters_per_curves,
intersections);
create_segments_from_intersections(inters_per_curves,
src_points_by_curve,
intersections,
is_cyclic,
all_segments,
all_segment_offset_data.as_mutable_span().drop_back(1));
store_segment_map_on_intersections(all_segments, intersections);
const OffsetIndices<int> segments_by_curve = offset_indices::accumulate_counts_to_offsets(
all_segment_offset_data);
Array<bool> segments_to_keep(all_segments.size(), true);
/* Remove the end segments unless that would delete the whole curve. */
editable_curves.foreach_index(
[&](const int curve_i) {
const IndexRange segment_range = segments_by_curve[curve_i];
if (segment_range.size() > 2) {
segments_to_keep[segment_range.first()] = false;
segments_to_keep[segment_range.last()] = false;
}
},
exec_mode::grain_size(128));
Array<SegmentConnections> segment_connections(all_segments.size(),
SegmentConnections(SEGMENT_CONNECTION_NULL));
create_connections_from_curves(
segments_by_curve, segments_to_keep, is_cyclic, segment_connections.as_mutable_span());
Vector<Segment> segments;
Vector<int> segment_offset_data;
Vector<bool> segment_reversed;
Vector<bool> cyclic;
follow_segment_connections(all_segments,
segments_to_keep,
segment_connections,
segments,
segment_offset_data,
segment_reversed,
cyclic);
const OffsetIndices<int> segment_offsets = OffsetIndices<int>(segment_offset_data);
bke::CurvesGeometry dst = create_curves_from_segments(
src, segments, segment_reversed, cyclic, segment_offsets);
if (!keep_caps) {
cut_caps(dst, segments, segment_reversed, cyclic, segment_offsets);
}
return dst;
}
} // namespace trim
/**
* Apply the stroke trim to a drawing.
*/

View file

@ -0,0 +1,995 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "testing/testing.h"
#include "MEM_guardedalloc.h"
#include "BLI_array.hh"
#include "BLI_array_utils.hh"
#include "BLI_bounds.hh"
#include "BLI_offset_indices.hh"
#include "BKE_attribute.hh"
#include "BKE_curves.hh"
#include "BKE_grease_pencil_fills.hh"
#include "BKE_gtest_base.hh"
#include "ED_grease_pencil.hh"
#include <fstream>
#include <iostream>
#include <sstream>
#include <type_traits>
/* Should tests draw their output to an HTML file? */
#define DO_DRAW 0
namespace blender::ed::greasepencil::tests {
using namespace blender::ed::greasepencil::boolean;
static void CSS_setup_style(std::ofstream &f)
{
constexpr int border_width = 5;
constexpr int stroke_width = 3;
constexpr int stroke_dasharray = 15;
f << ".ui-group {\n"
" border: "
<< border_width << "px solid black;\n"
<< " text-align: center;\n"
"}\n"
"\n";
f << ".ui-list {\n"
" align-items: center;\n"
" display: inline-flex;\n"
"}\n"
"\n";
f << ".polygon-A {\n"
" fill: blue;\n"
" fill-opacity: 0.25;\n"
" stroke: blue;\n"
" stroke-width: "
<< stroke_width
<< "px;\n"
" stroke-dasharray: "
<< stroke_dasharray
<< "px;\n"
"}\n";
f << ".polygon-B {\n"
" fill: red;\n"
" fill-opacity: 0.25;\n"
" stroke: red;\n"
" stroke-width: "
<< stroke_width
<< "px;\n"
" stroke-dasharray: "
<< stroke_dasharray
<< "px;\n"
"}\n";
f << ".polygon-C {\n"
" fill: green;\n"
" stroke: black;\n"
" stroke-width: "
<< stroke_width + 1
<< "px;\n"
" fill-opacity: 0.75;\n"
"}\n"
"\n";
f << ".cut-A {\n"
" fill: none;\n"
" stroke: blue;\n"
" stroke-width: "
<< stroke_width
<< "px;\n"
" stroke-dasharray: "
<< stroke_dasharray
<< "px;\n"
"}\n";
f << ".cut-B {\n"
" fill: red;\n"
" stroke: red;\n"
" fill-opacity: 0.25;\n"
" stroke-width: "
<< stroke_width
<< "px;\n"
" stroke-dasharray: "
<< stroke_dasharray
<< "px;\n"
"}\n";
f << ".cut-C {\n"
" fill: none;\n"
" stroke: black;\n"
" stroke-width: "
<< stroke_width + 1
<< "px;\n"
"}\n";
}
class SVGMapping {
public:
float2 topleft;
float scale;
float view_width;
float view_height;
float SX(const float x) const
{
return ((x - topleft[0]) * scale);
}
float SY(const float y) const
{
return ((topleft[1] - y) * scale);
}
SVGMapping(const Bounds<float2> &bounds)
{
constexpr int max_draw_width = 600;
constexpr int max_draw_height = 400;
const float draw_margin = (bounds.size().x + bounds.size().y) * 0.05;
Bounds<float2> bounds_padded = bounds;
bounds_padded.pad(draw_margin);
topleft = float2(bounds_padded.min.x, bounds_padded.max.y);
const float width = bounds_padded.size().x;
const float height = bounds_padded.size().y;
const float aspect = height / width;
view_width = max_draw_width;
view_height = int(view_width * aspect);
if (view_height > max_draw_height) {
view_height = max_draw_height;
view_width = int(view_height / aspect);
}
scale = view_width / width;
}
};
static void SVG_add_path(std::ofstream &f,
const std::string &class_name,
const Span<float2> &positions,
const GroupedSpan<int> shapes,
const IndexMask &shape_mask,
const OffsetIndices<int> points_by_curve,
const VArraySpan<bool> &cyclic,
const SVGMapping &mapping)
{
shape_mask.foreach_index([&](const int64_t shape_i) {
f << "<path class = \"" << class_name << "\" d = \"";
const Span<int> shape = shapes[shape_i];
for (const int pos : shape.index_range()) {
const int curve_i = shape[pos];
if (pos != 0) {
f << " ";
}
f << "M ";
const IndexRange points = points_by_curve[curve_i];
for (const int i : points.index_range()) {
const float2 &pos = positions[points[i]];
if (i == 1) {
f << " L ";
}
else if (i != 0) {
f << ", ";
}
f << mapping.SX(pos[0]) << "," << mapping.SY(pos[1]);
}
if (cyclic[curve_i]) {
f << " Z";
}
}
f << "\"";
f << " fill-rule=\"evenodd\"";
f << "/>\n";
});
}
static bool draw_append = false; /* Will be set to true after first call. */
static std::ofstream get_file_stream()
{
constexpr const char *drawfile = "./boolean_curves_test_draw.html";
std::ofstream f;
if (draw_append) {
f.open(drawfile, std::ios_base::app);
}
else {
f.open(drawfile);
}
if (!f) {
std::cout << "Could not open file " << drawfile << "\n";
return f;
}
if (!draw_append) {
f << "<!DOCTYPE html>\n";
f << "<style>\n";
CSS_setup_style(f);
f << "</style>\n";
}
draw_append = true;
return f;
}
static void draw_divider_start(const std::string &label)
{
if (!DO_DRAW) {
return;
}
std::ofstream f = get_file_stream();
if (!f) {
return;
}
f << "<div class=\"ui-group\">\n";
f << "<h1>" << label << "</h1>\n";
f << "<div class=\"ui-list\">\n";
}
static void draw_divider_end()
{
if (!DO_DRAW) {
return;
}
std::ofstream f = get_file_stream();
if (!f) {
return;
}
/* Exit `ui-list` */
f << "</div>\n";
/* Exit `ui-group` */
f << "</div>\n";
}
static float2 project_fn(const float3 position)
{
return float2(position.x, position.y);
};
static void draw_results(const std::string &label,
const std::string &type,
const bke::CurvesGeometry &src_curves,
const bke::CurvesGeometry &dst_curves,
const IndexMask &clipping_shapes)
{
using namespace bke::greasepencil;
if (!DO_DRAW) {
return;
}
std::ofstream f = get_file_stream();
if (!f) {
return;
}
const OffsetIndices<int> src_points_by_curve = src_curves.points_by_curve();
const OffsetIndices<int> dst_points_by_curve = dst_curves.points_by_curve();
const VArraySpan<bool> src_cyclic = src_curves.cyclic();
const VArraySpan<bool> dst_cyclic = dst_curves.cyclic();
const bke::AttributeAccessor src_attributes = src_curves.attributes();
const bke::AttributeAccessor dst_attributes = dst_curves.attributes();
Array<float2> src_positions_2d(src_curves.points_num());
const Span<float3> src_positions = src_curves.positions();
for (const int i : src_curves.points_range()) {
src_positions_2d[i] = project_fn(src_positions[i]);
}
Array<float2> dst_positions_2d(dst_curves.points_num());
const Span<float3> dst_positions = dst_curves.positions();
for (const int i : dst_curves.points_range()) {
dst_positions_2d[i] = project_fn(dst_positions[i]);
}
const VArray<int> src_fill_ids = *src_attributes.lookup<int>("fill_id", bke::AttrDomain::Curve);
const VArray<int> dst_fill_ids = *dst_attributes.lookup<int>("fill_id", bke::AttrDomain::Curve);
const ShapeData src_shape_data = shapes_from_fill_ids(src_fill_ids, src_curves.curves_num());
const ShapeData dst_shape_data = shapes_from_fill_ids(dst_fill_ids, dst_curves.curves_num());
const GroupedSpan<int> src_shapes = src_shape_data.shapes();
const GroupedSpan<int> dst_shapes = dst_shape_data.shapes();
IndexMaskMemory memory;
const IndexMask subject_shapes = clipping_shapes.complement(src_shapes.index_range(), memory);
const SVGMapping mapping = SVGMapping(*bounds::min_max(src_positions_2d.as_span()));
f << "<div>\n";
f << "<svg width=\"" << mapping.view_width << "\" height=\"" << mapping.view_height << "\">\n";
SVG_add_path(f,
type + "-A",
src_positions_2d,
src_shapes,
subject_shapes,
src_points_by_curve,
src_cyclic,
mapping);
SVG_add_path(f,
type + "-B",
src_positions_2d,
src_shapes,
clipping_shapes,
src_points_by_curve,
src_cyclic,
mapping);
SVG_add_path(f,
type + "-C",
dst_positions_2d,
dst_shapes,
dst_shapes.index_range(),
dst_points_by_curve,
dst_cyclic,
mapping);
f << "</svg>\n";
f << "<h2>" << label << "</h2>\n";
f << "</div>\n";
}
static bke::CurvesGeometry create_test_curves(const Span<int> offsets,
const Span<float2> points,
const Span<int> fill_ids,
const Span<bool> cyclic)
{
BLI_assert(!offsets.is_empty());
const int curves_num = offsets.size() - 1;
const int points_num = offsets.last();
BLI_assert(cyclic.size() == curves_num);
BLI_assert(fill_ids.size() == curves_num);
bke::CurvesGeometry curves(points_num, curves_num);
curves.offsets_for_write().copy_from(offsets);
curves.cyclic_for_write().copy_from(cyclic);
bke::MutableAttributeAccessor attributes = curves.attributes_for_write();
bke::SpanAttributeWriter<int> fill_id_writer = attributes.lookup_or_add_for_write_span<int>(
"fill_id", bke::AttrDomain::Curve);
fill_id_writer.span.copy_from(fill_ids);
fill_id_writer.finish();
MutableSpan<float3> positions = curves.positions_for_write();
for (const int i : curves.points_range()) {
positions[i] = float3(points[i], 0.0f);
}
return curves;
}
static void expect_boolean_result_coord(const bke::CurvesGeometry &dst_curves,
const Array<Vector<float2>> &expected_points)
{
Array<float2> dst_positions_2d(dst_curves.points_num());
const Span<float3> dst_positions = dst_curves.positions();
for (const int i : dst_curves.points_range()) {
dst_positions_2d[i] = project_fn(dst_positions[i]);
}
const OffsetIndices<int> points_by_curve = dst_curves.points_by_curve();
EXPECT_EQ(dst_curves.curves_num(), expected_points.size());
if (dst_curves.curves_num() != expected_points.size()) {
return;
}
int total_size = 0;
for (const int i : expected_points.index_range()) {
total_size += expected_points[i].size();
}
EXPECT_EQ(dst_positions_2d.size(), total_size);
if (dst_positions_2d.size() != total_size) {
return;
}
for (const int curve_i : points_by_curve.index_range()) {
const IndexRange points = points_by_curve[curve_i];
const Span<float2> expected_sub_points = expected_points[curve_i];
EXPECT_EQ(expected_sub_points.size(), points.size());
if (expected_sub_points.size() != points.size()) {
return;
}
for (const int i : points.index_range()) {
const float2 &point = dst_positions_2d[points[i]];
const float2 &expected_point = expected_sub_points[i];
EXPECT_NEAR(point.x, expected_point.x, 1e-4);
EXPECT_NEAR(point.y, expected_point.y, 1e-4);
}
}
}
static bke::CurvesGeometry test_curve_boolean(const Operation opt,
const bke::CurvesGeometry &src_curves,
const Span<int> fill_ids,
const IndexMask &clipping_shapes)
{
using namespace bke::greasepencil;
CurveBooleanOpParameters op_params;
op_params.boolean_mode = opt;
op_params.keep_caps = true;
op_params.skip_clipping_attributes = false;
op_params.separate_islands = false;
const ShapeData shapes_data = shapes_from_fill_ids(VArray<int>::from_span(fill_ids),
src_curves.curves_num());
const GroupedSpan<int> shapes = shapes_data.shapes();
return curve_boolean(op_params, src_curves, project_fn, shapes, clipping_shapes);
}
class GreasePencilBooleanTest : public bke::BlenderGTestBase {};
TEST_F(GreasePencilBooleanTest, Squares)
{
draw_divider_start("Squares");
const Array<float2> points = {{0, 0}, {2, 0}, {2, 2}, {0, 2}, {1, 1}, {3, 1}, {3, 3}, {1, 3}};
const Array<int> points_by_curve = {0, 4, 8};
const Array<int> fill_ids = {1, 2};
const Array<bool> is_cyclic = {true, true};
const IndexRange clipping_shapes = IndexRange(1, 1);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Intersect, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {{{2, 1}, {2, 2}, {1, 2}, {1, 1}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Intersection", "polygon", src_curves, dst_curves, clipping_shapes);
}
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Union, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{1, 2}, {0, 2}, {0, 0}, {2, 0}, {2, 1}, {3, 1}, {3, 3}, {1, 3}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Union", "polygon", src_curves, dst_curves, clipping_shapes);
}
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{1, 2}, {0, 2}, {0, 0}, {2, 0}, {2, 1}, {1, 1}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Difference", "polygon", src_curves, dst_curves, clipping_shapes);
}
draw_divider_end();
}
TEST_F(GreasePencilBooleanTest, Simple)
{
draw_divider_start("Simple");
/**
* This is a replica of Fig. 10 from
* Greiner, Günther; Kai Hormann (1998). "Efficient clipping of arbitrary polygons". ACM
* Transactions on Graphics. 17 (2): 71-83.
*/
const Array<float2> points = {
{0, 6}, {8, 6}, {8, 3}, {0, 3}, {6, 0}, {6, 4}, {4, 2}, {2, 4}, {2, 0}};
const Array<int> points_by_curve = {0, 4, 9};
const Array<bool> is_cyclic = {true, true};
const Array<int> fill_ids = {1, 2};
const IndexRange clipping_shapes = IndexRange(1, 1);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Intersect, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {{{6, 3}, {5, 3}, {6, 4}},
{{3, 3}, {2, 3}, {2, 4}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Intersection", "polygon", src_curves, dst_curves, clipping_shapes);
}
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Union, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{2, 3}, {0, 3}, {0, 6}, {8, 6}, {8, 3}, {6, 3}, {6, 0}, {2, 0}},
{{5, 3}, {3, 3}, {4, 2}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Union", "polygon", src_curves, dst_curves, clipping_shapes);
}
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{2, 3}, {0, 3}, {0, 6}, {8, 6}, {8, 3}, {6, 3}, {6, 4}, {5, 3}, {3, 3}, {2, 4}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Difference", "polygon", src_curves, dst_curves, clipping_shapes);
}
draw_divider_end();
}
TEST_F(GreasePencilBooleanTest, Complex)
{
draw_divider_start("Complex");
/**
* This is a replica of Fig. 16 from
* Greiner, Günther; Kai Hormann (1998). "Efficient clipping of arbitrary polygons". ACM
* Transactions on Graphics. 17 (2): 71-83.
*/
const Array<float2> points = {
{14, 1}, {0, 5}, {14, 10}, {5, 6}, {14, 6}, {5, 5}, {9, 13}, {13, 0}, {9, 9}, {6, 0}};
const Array<int> points_by_curve = {0, 6, 10};
const Array<bool> is_cyclic = {true, true};
const Array<int> fill_ids = {1, 2};
const IndexRange clipping_shapes = IndexRange(1, 1);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Intersect, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{12.5662, 1.4096}, {12.3454, 1.4727}, {12.2, 1.8}, {12.4851, 1.6732}},
{{7, 3}, {6.7113, 3.0824}, {6.9534, 4.1317}, {7.3225, 3.9677}},
{{7.7964, 7.7844}, {8.7027, 8.1081}, {8.5217, 7.5652}, {7.6571, 7.1809}},
{{9.3013, 8.3219}, {10.3267, 8.6881}, {10.4135, 8.4060}, {9.4536, 7.9793}},
{{7.3846, 6}, {8, 6}, {7.7692, 5.3076}, {7.2105, 5.2456}},
{{10.3333, 6}, {11.1538, 6}, {11.2479, 5.6942}, {10.5058, 5.6117}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Intersection", "polygon", src_curves, dst_curves, clipping_shapes);
}
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Union, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{12.4851, 1.6732},
{14, 1},
{12.5662, 1.4096},
{13, 0},
{12.3454, 1.4727},
{7, 3},
{6, 0},
{6.7113, 3.0824},
{0, 5},
{7.7964, 7.7844},
{9, 13},
{10.3267, 8.6881},
{14, 10},
{10.4135, 8.4060},
{11.1538, 6},
{14, 6},
{11.2479, 5.6942}},
{{8.7027, 8.1081}, {9.3013, 8.3219}, {9, 9}},
{{9.4536, 7.9793}, {8.5217, 7.5652}, {8, 6}, {10.3333, 6}},
{{7.6571, 7.1809}, {5, 6}, {7.3846, 6}},
{{10.5058, 5.6117}, {7.7692, 5.3076}, {7.3225, 3.9677}, {12.2, 1.8}},
{{7.2105, 5.2456}, {5, 5}, {6.9534, 4.1317}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Union", "polygon", src_curves, dst_curves, clipping_shapes);
}
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{12.4851, 1.6732}, {14, 1}, {12.5662, 1.4096}},
{{12.3454, 1.4727}, {7, 3}, {7.3225, 3.9677}, {12.2, 1.8}},
{{6.7113, 3.0824},
{0, 5},
{7.7964, 7.7844},
{7.6571, 7.1809},
{5, 6},
{7.3846, 6},
{7.2105, 5.2456},
{5, 5},
{6.9534, 4.1317}},
{{8.7027, 8.1081}, {9.3013, 8.3219}, {9.4536, 7.9793}, {8.5217, 7.5652}},
{{10.3267, 8.6881}, {14, 10}, {10.4135, 8.4060}},
{{8, 6}, {10.3333, 6}, {10.5058, 5.6117}, {7.7692, 5.3076}},
{{11.1538, 6}, {14, 6}, {11.2479, 5.6942}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Difference", "polygon", src_curves, dst_curves, clipping_shapes);
}
draw_divider_end();
}
TEST_F(GreasePencilBooleanTest, Last_Edge_Loop)
{
draw_divider_start("Last Edge Loop");
/**
* These shapes are designed to test the following:
* 1: Intersection with the last edge.
* 2: Segment connected through a full loop around.
* 3: Multiple intersection on one edge not in order.
* 4: Having a self intersection.
*/
const Array<float2> points = {
{0, 5}, {0, 0}, {7, 0}, {7, 5}, {2, 3}, {0, 7}, {3, 7}, {6, 3}, {7, 6}, {3, 3}, {2, 6}};
const Array<int> points_by_curve = {0, 4, 11};
const Array<bool> is_cyclic = {true, true};
const Array<int> fill_ids = {1, 2};
const IndexRange clipping_shapes = IndexRange(1, 1);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Intersect, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{6.6666, 5}, {5.6666, 5}, {3, 3}, {2.3333, 5}, {4.5000, 5}, {6, 3}},
{{2, 5}, {1, 5}, {2, 3}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Intersection", "polygon", src_curves, dst_curves, clipping_shapes);
}
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Union, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {{{1, 5},
{0, 5},
{0, 0},
{7, 0},
{7, 5},
{6.6666, 5},
{7, 6},
{5.6666, 5},
{4.5, 5},
{3, 7},
{0, 7}},
{{2.3333, 5}, {2, 5}, {2, 6}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Union", "polygon", src_curves, dst_curves, clipping_shapes);
}
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {{{1, 5},
{0, 5},
{0, 0},
{7, 0},
{7, 5},
{6.6666, 5},
{6, 3},
{4.5, 5},
{5.6666, 5},
{3, 3},
{2.3333, 5},
{2, 5},
{2, 3}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Difference", "polygon", src_curves, dst_curves, clipping_shapes);
}
draw_divider_end();
}
TEST_F(GreasePencilBooleanTest, Simple_Cuts)
{
draw_divider_start("Cuts");
{
const Array<float2> points = {
{5, 7}, {3, 6}, {0, 2}, {0, 0}, {1, 6}, {3, 4}, {3, 1}, {0, 4}, {2, 3}};
const Array<int> points_by_curve = {0, 4, 9};
const Array<bool> is_cyclic = {false, true};
const Array<int> fill_ids = {0, 1};
const IndexRange clipping_shapes = IndexRange(1, 1);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {{{5, 7}, {3, 6}, {2.14286, 4.85714}},
{{1.61538, 4.15385}, {1.09091, 3.45455}},
{{0.857143, 3.14286}, {0, 2}, {0, 0}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Simple Cut 1", "cut", src_curves, dst_curves, clipping_shapes);
}
{
const Array<float2> points = {{5, 5}, {3, 5}, {1, 3}, {1, 1}, {5, 6}, {6, 5}, {1, 0}, {0, 1}};
const Array<int> points_by_curve = {0, 4, 8};
const Array<bool> is_cyclic = {false, true};
const Array<int> fill_ids = {0, 1};
const IndexRange clipping_shapes = IndexRange(1, 1);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {{{4, 5}, {3, 5}, {1, 3}, {1, 2}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Simple Cut 2", "cut", src_curves, dst_curves, clipping_shapes);
}
{
const Array<float2> points = {{6, 8},
{4, 7},
{1, 3},
{1, 1},
{3, 7},
{5, 5},
{1, 0},
{0, 4},
{2, 3},
{1, 5},
{3, 4},
{2, 6},
{4, 5}};
const Array<int> points_by_curve = {0, 4, 13};
const Array<bool> is_cyclic = {false, true};
const Array<int> fill_ids = {0, 1};
const IndexRange clipping_shapes = IndexRange(1, 1);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {{{6, 8}, {4, 7}, {3.57143, 6.42857}},
{{3.4, 6.2}, {2.90909, 5.54545}},
{{2.5, 5}, {2.09091, 4.45455}},
{{1.6, 3.8}, {1.27273, 3.36364}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Simple Cut 3", "cut", src_curves, dst_curves, clipping_shapes);
}
{
const Array<float2> points = {{6, 7},
{4, 6},
{1, 2},
{1, 0},
{0, 4},
{2, 2},
{7, 8},
{3, 7},
{4, 5},
{2, 6},
{3, 4},
{1, 5},
{2, 3}};
const Array<int> points_by_curve = {0, 4, 13};
const Array<bool> is_cyclic = {false, true};
const Array<int> fill_ids = {0, 1};
const IndexRange clipping_shapes = IndexRange(1, 1);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {{{3.7, 5.6}, {3.45455, 5.27273}},
{{2.8, 4.4}, {2.63636, 4.18182}},
{{1.9, 3.2}, {1.81818, 3.09091}},
{{1.42857, 2.57143}, {1, 2}, {1, 0}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Simple Cut 4", "cut", src_curves, dst_curves, clipping_shapes);
}
{
const Array<float2> points = {
{6, 5}, {4, 5}, {1, 2}, {1, 0}, {1, 4}, {3, 1}, {5, 3}, {2, 5}, {3, 3}};
const Array<int> points_by_curve = {0, 4, 9};
const Array<bool> is_cyclic = {true, true};
const Array<int> fill_ids = {0, 1};
const IndexRange clipping_shapes = IndexRange(1, 1);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {{{4.4, 3.4}, {6, 5}, {4, 5}, {3.2, 4.2}},
{{2.66667, 3.66667}, {2.33333, 3.33333}},
{{1.8, 2.8}, {1, 2}, {1, 0}, {2.6, 1.6}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Cyclical Cut", "cut", src_curves, dst_curves, clipping_shapes);
}
draw_divider_end();
}
TEST_F(GreasePencilBooleanTest, Square_With_Hole)
{
draw_divider_start("Square With Hole");
const Array<float2> points = {{0, 0},
{0, 5},
{5, 5},
{5, 0},
{1, 1},
{1, 4},
{4, 4},
{4, 1},
{2, 2},
{2, 7},
{7, 7},
{7, 2}};
const Array<int> points_by_curve = {0, 4, 8, 12};
const Array<bool> is_cyclic = {true, true, true};
const Array<int> fill_ids = {1, 1, 2};
const IndexRange clipping_shapes = IndexRange::from_begin_end(1, 2);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{5, 2}, {5, 0}, {0, 0}, {0, 5}, {2, 5}, {2, 4}, {1, 4}, {1, 1}, {4, 1}, {4, 2}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Difference", "polygon", src_curves, dst_curves, clipping_shapes);
}
draw_divider_end();
}
TEST_F(GreasePencilBooleanTest, Squares_With_Holes)
{
draw_divider_start("Squares With Holes");
const Array<float2> points = {{0, 0},
{0, 5},
{5, 5},
{5, 0},
{1, 1},
{1, 4},
{4, 4},
{4, 1},
{2, 2},
{2, 7},
{7, 7},
{7, 2},
{3, 3},
{3, 6},
{6, 6},
{6, 3}};
const Array<int> points_by_curve = {0, 4, 8, 12, 16};
const Array<bool> is_cyclic = {true, true, true, true};
const Array<int> fill_ids = {1, 1, 2, 2};
const IndexRange clipping_shapes = IndexRange::from_begin_end(1, 2);
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
{
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{5, 2}, {5, 0}, {0, 0}, {0, 5}, {2, 5}, {2, 4}, {1, 4}, {1, 1}, {4, 1}, {4, 2}},
{{3, 5}, {5, 5}, {5, 3}, {4, 3}, {4, 4}, {3, 4}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("Difference", "polygon", src_curves, dst_curves, clipping_shapes);
}
draw_divider_end();
}
TEST_F(GreasePencilBooleanTest, Multiple_Shapes)
{
draw_divider_start("Multiple Shapes");
const Array<float2> points = {{0, 2},
{0, 7},
{5, 7},
{5, 2},
{2, 0},
{2, 5},
{7, 5},
{7, 0},
{3, 3},
{3, 8},
{8, 8},
{8, 3}};
const Array<int> points_by_curve = {0, 4, 8, 12};
const Array<bool> is_cyclic = {true, true, true};
const Array<int> fill_ids = {1, 2, 3};
const bke::CurvesGeometry src_curves = create_test_curves(
points_by_curve, points, fill_ids, is_cyclic);
/**
* Multiple separate but intersecting subject shapes.
* The two subject shapes should be affected by the clipping shape, but not join into one.
*/
{
const IndexRange clipping_shapes = IndexRange::from_begin_end(2, 3);
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Intersect, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {{{3, 7}, {5, 7}, {5, 3}, {3, 3}},
{{3, 5}, {7, 5}, {7, 3}, {3, 3}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("2 Subjects Intersection", "polygon", src_curves, dst_curves, clipping_shapes);
}
{
const IndexRange clipping_shapes = IndexRange::from_begin_end(2, 3);
const bke::CurvesGeometry dst_curves = test_curve_boolean(
Operation::Difference, src_curves, fill_ids, clipping_shapes);
const Array<Vector<float2>> expected_points = {
{{5, 3}, {5, 2}, {0, 2}, {0, 7}, {3, 7}, {3, 3}},
{{7, 3}, {7, 0}, {2, 0}, {2, 5}, {3, 5}, {3, 3}}};
expect_boolean_result_coord(dst_curves, expected_points);
draw_results("2 Subjects Difference", "polygon", src_curves, dst_curves, clipping_shapes);
}
draw_divider_end();
}
} // namespace blender::ed::greasepencil::tests

View file

@ -88,6 +88,7 @@ void ED_interpolatetool_modal_keymap(wmKeyConfig *keyconf);
void ED_grease_pencil_pentool_modal_keymap(wmKeyConfig *keyconf);
void GREASE_PENCIL_OT_stroke_trim(wmOperatorType *ot);
void GREASE_PENCIL_OT_stroke_carver(wmOperatorType *ot);
void ED_undosys_type_grease_pencil(UndoType *ut);
@ -979,6 +980,44 @@ bke::CurvesGeometry trim_curve_segment_ends(const bke::CurvesGeometry &src,
bool keep_caps);
}; // namespace trim
namespace boolean {
enum class Operation : int8_t {
/* Intersection of the Subject and the Clipping. */
Intersect,
/* Union of Subject and Clipping. */
Union,
/* Differences of Subject with Clipping. */
Difference,
};
struct CurveBooleanOpParameters {
Operation boolean_mode;
bool keep_caps;
bool skip_clipping_attributes;
bool separate_islands;
};
/* Project the point from local space to the clipping 2d space. */
using ProjectionFunc = FunctionRef<float2(const float3)>;
bke::CurvesGeometry curve_boolean(const CurveBooleanOpParameters op_params,
const bke::CurvesGeometry &curves,
ProjectionFunc project_fn,
GroupedSpan<int> shapes,
const IndexMask &clipping_shapes);
bke::CurvesGeometry curve_boolean_with_planes(const CurveBooleanOpParameters op_params,
const bke::CurvesGeometry &curves,
ProjectionFunc project_fn,
GroupedSpan<int> shapes,
Span<float4> curve_planes,
const IndexMask &clipping_shapes,
const float4x4 &layer_to_world,
const ARegion &region);
} // namespace boolean
void merge_layers(const GreasePencil &src_grease_pencil,
const Span<Vector<int>> src_layer_indices_by_dst_layer,
GreasePencil &dst_grease_pencil);