mirror of
https://github.com/blender/blender
synced 2026-09-29 04:37:17 +03:00
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:
parent
20a730de80
commit
302eb8bddc
17 changed files with 3692 additions and 700 deletions
3
release/datafiles/icons/ops.gpencil.carver.dat
Normal file
3
release/datafiles/icons/ops.gpencil.carver.dat
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:b8ded96a8b53a9df98193a8a9e332dbb4fbd1d8590599c9db7159a439894232a
|
||||
size 3986
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:3a74a6d883783b4b9b9ae920c504b6ac0b23a8921b906e1441d45a69575404a5
|
||||
size 1916
|
||||
oid sha256:e5a54e50e241616b3494e10615f139e8cc49e25795ecb2f536347f3c322b02a6
|
||||
size 1214
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
version https://git-lfs.github.com/spec/v1
|
||||
oid sha256:1bc4cb37ee64d304b4ebf0803a75872d218c05b4dca7442aeeb3b5cd667dca49
|
||||
size 2870795
|
||||
oid sha256:17678861c7798ce603f922c09ed564c6c8f0325b97e61b30e2f94ac569411843
|
||||
size 2859369
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
|
|
|
|||
1306
source/blender/editors/grease_pencil/intern/grease_pencil_boolean.cc
Normal file
1306
source/blender/editors/grease_pencil/intern/grease_pencil_boolean.cc
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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 ®ion,
|
||||
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(®ion, 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
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 ®ion);
|
||||
|
||||
} // namespace boolean
|
||||
|
||||
void merge_layers(const GreasePencil &src_grease_pencil,
|
||||
const Span<Vector<int>> src_layer_indices_by_dst_layer,
|
||||
GreasePencil &dst_grease_pencil);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue