diff --git a/scripts/modules/bl_keymap_utils/keymap_from_toolbar.py b/scripts/modules/bl_keymap_utils/keymap_from_toolbar.py index d2bcc07ab47..1f5f0ee17b0 100644 --- a/scripts/modules/bl_keymap_utils/keymap_from_toolbar.py +++ b/scripts/modules/bl_keymap_utils/keymap_from_toolbar.py @@ -186,6 +186,7 @@ def generate(context, space_type, *, use_fallback_keys=True, use_reset=True): 'WEIGHT_PAINT': "weight_tool", 'TEXTURE_PAINT': "image_tool", 'PAINT_GPENCIL': "gpencil_tool", + 'PAINT_GREASE_PENCIL': "gpencil_tool", 'VERTEX_GPENCIL': "gpencil_vertex_tool", 'SCULPT_GPENCIL': "gpencil_sculpt_tool", 'WEIGHT_GPENCIL': "gpencil_weight_tool", diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 16e3ddcc16b..69dd2b182aa 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -4595,6 +4595,21 @@ def km_grease_pencil_paint_mode(_params): return keymap +def km_grease_pencil_fill(_params): + items = [] + keymap = ( + "Grease Pencil Paint Mode (Fill)", + {"space_type": 'EMPTY', "region_type": 'WINDOW'}, + {"items": items}, + ) + + items.extend([ + ("grease_pencil.fill", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), + ]) + + return keymap + + def km_grease_pencil_edit_mode(params): items = [] keymap = ( @@ -8614,6 +8629,7 @@ def generate_keymaps(params=None): km_grease_pencil_stroke_vertex_replace(params), # Grease Pencil v3 km_grease_pencil_paint_mode(params), + km_grease_pencil_fill(params), km_grease_pencil_edit_mode(params), # Object mode. km_object_mode(params), diff --git a/scripts/startup/bl_ui/properties_paint_common.py b/scripts/startup/bl_ui/properties_paint_common.py index c3a3fb97afc..ca6ccd2cf9b 100644 --- a/scripts/startup/bl_ui/properties_paint_common.py +++ b/scripts/startup/bl_ui/properties_paint_common.py @@ -1398,32 +1398,33 @@ def brush_basic_grease_pencil_paint_settings(layout, context, brush, *, compact= grease_pencil_tool = brush.gpencil_tool - UnifiedPaintPanel.prop_unified( - layout, - context, - brush, - "size", - unified_name="use_unified_size", - pressure_name="use_pressure_size", - text="Radius", - slider=True, - header=compact, - ) + if grease_pencil_tool != 'FILL': + UnifiedPaintPanel.prop_unified( + layout, + context, + brush, + "size", + unified_name="use_unified_size", + pressure_name="use_pressure_size", + text="Radius", + slider=True, + header=compact, + ) - if brush.use_pressure_size and not compact: - col = layout.column() - col.template_curve_mapping(gp_settings, "curve_sensitivity", brush=True, use_negative_slope=True) + if brush.use_pressure_size and not compact: + col = layout.column() + col.template_curve_mapping(gp_settings, "curve_sensitivity", brush=True, use_negative_slope=True) - UnifiedPaintPanel.prop_unified( - layout, - context, - brush, - "strength", - unified_name="use_unified_strength", - pressure_name="use_pressure_strength", - slider=True, - header=compact, - ) + UnifiedPaintPanel.prop_unified( + layout, + context, + brush, + "strength", + unified_name="use_unified_strength", + pressure_name="use_pressure_strength", + slider=True, + header=compact, + ) if brush.use_pressure_strength and not compact: col = layout.column() @@ -1441,7 +1442,12 @@ def brush_basic_grease_pencil_paint_settings(layout, context, brush, *, compact= if gp_settings.eraser_mode == "HARD": layout.prop(gp_settings, "use_keep_caps_eraser") layout.prop(gp_settings, "use_active_layer_only") - + elif grease_pencil_tool == 'FILL': + if brush.gpencil_settings.fill_mode == 'FLOOD': + layout.prop(brush.gpencil_settings, "fill_factor") + layout.prop(brush.gpencil_settings, "dilate") + layout.prop(brush, "size", text="Thickness") + layout.popover("VIEW3D_PT_tools_grease_pencil_fill_options") def brush_basic_gpencil_sculpt_settings(layout, _context, brush, *, compact=False): if brush is None: diff --git a/scripts/startup/bl_ui/space_toolsystem_toolbar.py b/scripts/startup/bl_ui/space_toolsystem_toolbar.py index 42e774ed35e..e3eb3ccef69 100644 --- a/scripts/startup/bl_ui/space_toolsystem_toolbar.py +++ b/scripts/startup/bl_ui/space_toolsystem_toolbar.py @@ -1798,6 +1798,15 @@ class _defs_paint_grease_pencil: data_block='DRAW', ) + @ToolDef.from_fn + def fill(): + return dict( + idname="builtin_brush.Fill", + label="Fill", + icon="brush.gpencil_draw.fill", + data_block='FILL', + ) + @ToolDef.from_fn def erase(): return dict( @@ -3171,6 +3180,7 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel): _defs_view3d_generic.cursor, None, _defs_paint_grease_pencil.draw, + _defs_paint_grease_pencil.fill, _defs_paint_grease_pencil.erase, ], 'PAINT_GPENCIL': [ diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index ef75e104829..36dd3113a45 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -593,12 +593,12 @@ class _draw_tool_settings_context_mode: if brush is None: return False + grease_pencil_tool = brush.gpencil_tool + row = layout.row(align=True) row.template_ID_preview(paint, "brush", rows=3, cols=8, hide_buttons=True) - grease_pencil_tool = brush.gpencil_tool - - if grease_pencil_tool == 'DRAW': + if grease_pencil_tool in {'DRAW', 'FILL'}: from bl_ui.properties_paint_common import ( brush_basic__draw_color_selector, ) diff --git a/scripts/startup/bl_ui/space_view3d_toolbar.py b/scripts/startup/bl_ui/space_view3d_toolbar.py index babbb5f1cc6..e6e1550f978 100644 --- a/scripts/startup/bl_ui/space_view3d_toolbar.py +++ b/scripts/startup/bl_ui/space_view3d_toolbar.py @@ -2659,6 +2659,102 @@ class VIEW3D_PT_tools_grease_pencil_v3_brush_mix_palette(View3DPanel, Panel): col.template_palette(settings, "palette", color=True) +class VIEW3D_PT_tools_grease_pencil_fill_options(View3DPanel, Panel): + bl_context = ".greasepencil_paint" + bl_label = "Advanced" + bl_parent_id = 'VIEW3D_PT_tools_grease_pencil_brush_settings' + bl_category = "Tool" + bl_options = {'DEFAULT_CLOSED'} + bl_ui_units_x = 15 + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + tool_settings = context.tool_settings + brush = tool_settings.gpencil_paint.brush + if brush is None: + return + gp_settings = brush.gpencil_settings + is_flood_fill = (gp_settings.fill_mode == 'FLOOD') + + col = layout.column(align=True) + col.prop(gp_settings, "fill_mode") + + col.separator() + col.label(text="Scope") + + col = layout.column(align=True) + col.prop(gp_settings, "fill_layer_mode", text="Layers") + + if is_flood_fill: + col.separator() + row = col.row(align=True) + row.prop(gp_settings, "fill_draw_mode", text="Boundary", + text_ctxt=i18n_contexts.id_gpencil) + row.prop( + gp_settings, + "show_fill_boundary", + icon='HIDE_OFF' if gp_settings.show_fill_boundary else 'HIDE_ON', + text="", + ) + + col.separator() + col.prop(gp_settings, "fill_simplify_level", text="Simplify") + + if gp_settings.fill_draw_mode != 'STROKE': + col = layout.column(align=False, heading="Ignore Transparent") + col.use_property_decorate = False + row = col.row(align=True) + sub = row.row(align=True) + sub.prop(gp_settings, "show_fill", text="") + sub = sub.row(align=True) + sub.active = gp_settings.show_fill + sub.prop(gp_settings, "fill_threshold", text="") + + if is_flood_fill: + col.separator() + col.prop(gp_settings, "use_fill_limit") + + +class VIEW3D_PT_tools_grease_pencil_fill_gap_closure(View3DPanel, Panel): + bl_context = ".greasepencil_paint" + bl_parent_id = 'VIEW3D_PT_tools_grease_pencil_fill_options' + bl_label = "Gap Closure" + bl_category = "Tool" + + @classmethod + def poll(cls, context): + brush = context.tool_settings.gpencil_paint.brush + return brush is not None and brush.gpencil_tool == 'FILL' + + def draw(self, context): + layout = self.layout + layout.use_property_split = True + layout.use_property_decorate = False + + tool_settings = context.tool_settings + brush = tool_settings.gpencil_paint.brush + gp_settings = brush.gpencil_settings + is_geometry_fill = (gp_settings.fill_mode == 'GEOMETRY') + + col = layout.column(align=True) + + if is_geometry_fill: + col.prop(gp_settings, "fill_proximity_distance") + col.separator() + + col.prop(gp_settings, "fill_extend_mode", text="End Mode") + col.separator() + col.prop(gp_settings, "extend_stroke_factor", text="Size") + + col.separator() + col.prop(gp_settings, "show_fill_extend", text="Visual Aids") + if gp_settings.fill_extend_mode == 'EXTEND': + col.prop(gp_settings, "use_collide_strokes") + + classes = ( VIEW3D_MT_brush_context_menu, VIEW3D_MT_brush_gpencil_context_menu, @@ -2761,6 +2857,9 @@ classes = ( VIEW3D_PT_tools_grease_pencil_brush_vertex_color, VIEW3D_PT_tools_grease_pencil_brush_vertex_palette, VIEW3D_PT_tools_grease_pencil_brush_vertex_falloff, + + VIEW3D_PT_tools_grease_pencil_fill_options, + VIEW3D_PT_tools_grease_pencil_fill_gap_closure, ) if __name__ == "__main__": # only for live edit. diff --git a/source/blender/editors/grease_pencil/CMakeLists.txt b/source/blender/editors/grease_pencil/CMakeLists.txt index 2c15a93d16b..6374a2e4873 100644 --- a/source/blender/editors/grease_pencil/CMakeLists.txt +++ b/source/blender/editors/grease_pencil/CMakeLists.txt @@ -4,6 +4,7 @@ set(INC ../include + ../../blenfont ../../blenkernel ../../blentranslation ../../bmesh @@ -21,8 +22,15 @@ set(INC_SYS ) set(SRC + intern/grease_pencil_fill.hh + intern/grease_pencil_fill_geometry.hh + intern/grease_pencil_fill_flood.hh + intern/grease_pencil_add.cc intern/grease_pencil_edit.cc + intern/grease_pencil_fill.cc + intern/grease_pencil_fill_flood.cc + intern/grease_pencil_fill_geometry.cc intern/grease_pencil_frames.cc intern/grease_pencil_geom.cc intern/grease_pencil_layers.cc diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_fill.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_fill.cc new file mode 100644 index 00000000000..7b1f37935fc --- /dev/null +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_fill.cc @@ -0,0 +1,316 @@ +/* SPDX-FileCopyrightText: 2023 Blender Foundation + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edgreasepencil + */ + +#include "grease_pencil_fill.hh" + +namespace blender::ed::greasepencil::fill { + +/** + * Draw gap closure lines on an overlay in the 3D viewport. + */ +void draw_overlay(const bContext * /*C*/, ARegion *region, void *arg) +{ + FillOperation &fill_op = *static_cast(arg); + + /* Draw only in the region that originated the operator. */ + if (region != fill_op.vc.region) { + return; + } + + /* Anything to draw? */ + if (!(fill_op.use_gap_close_extend || fill_op.use_gap_close_radius || + fill_op.use_gap_close_proximity)) + { + return; + } + + const uint shdr_pos = GPU_vertformat_attr_add( + immVertexFormat(), "pos", GPU_COMP_F32, 2, GPU_FETCH_FLOAT); + immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR); + + GPU_line_width(1.5f); + GPU_blend(GPU_BLEND_ALPHA); + GPU_line_smooth(true); + + /* Draw indications of curve proximities. */ + if (fill_op.use_gap_close_proximity) { + /* Draw filled circle at start, middle and end point of each curve. */ + int point_indices[3]; + immUniformColor4fv(fill_op.gap_proximity_color); + for (const int curve_i : fill_op.curves_2d.point_offset.index_range()) { + if (fill_op.curves_2d.point_size[curve_i] < 2) { + continue; + } + + point_indices[0] = 0; + point_indices[1] = fill_op.curves_2d.point_size[curve_i] - 1; + point_indices[2] = int(point_indices[1] / 2); + const int point_count = (point_indices[1] >= 2) ? 3 : 2; + const int point_offset = fill_op.curves_2d.point_offset[curve_i]; + + for (int point = 0; point < point_count; point++) { + const int point_i = point_offset + point_indices[point]; + imm_draw_circle_fill_2d(shdr_pos, + fill_op.curves_2d.points_2d[point_i][0], + fill_op.curves_2d.points_2d[point_i][1], + fill_op.proximity_distance, + 40); + } + } + } + + /* Draw curve end extensions. */ + if (fill_op.use_gap_close_extend) { + /* Draw extensions of full length. */ + for (const int ext_i : fill_op.extensions_2d.point_offset.index_range()) { + /* Skip intersected extensions or extensions of cyclic curves. */ + if ((fill_op.extensions_stop_at_first_intersection && + fill_op.extension_has_intersection[ext_i]) || + fill_op.extensions_2d.is_cyclic[ext_i]) + { + continue; + } + + if (fill_op.extension_has_intersection[ext_i]) { + immUniformColor3fv(fill_op.gap_closed_color); + } + else { + immUniformColor3fv(fill_op.gap_closure_color); + } + + const int point_i = fill_op.extensions_2d.point_offset[ext_i]; + immBegin(GPU_PRIM_LINES, 2); + immVertex2fv(shdr_pos, fill_op.extensions_2d.points_2d[point_i]); + immVertex2fv(shdr_pos, fill_op.extensions_2d.points_2d[point_i + 1]); + immEnd(); + } + + /* Draw shortened extensions (that intersect a curve or other extension). */ + if (fill_op.extensions_stop_at_first_intersection) { + immUniformColor3fv(fill_op.gap_closed_color); + int ext_prev = -1; + for (const auto &intersection : fill_op.extension_intersections) { + const int ext_i = intersection.extension_index; + const int point_i = fill_op.extensions_2d.point_offset[ext_i]; + if (ext_i == ext_prev) { + continue; + } + ext_prev = ext_i; + + /* Limit the end extension when it is intersecting something else. */ + const float distance = intersection.distance[0]; + const float2 p0 = fill_op.extensions_2d.points_2d[point_i]; + const float2 p1 = fill_op.extensions_2d.points_2d[point_i + 1]; + const float2 p_vec = p1 - p0; + + immBegin(GPU_PRIM_LINES, 2); + immVertex2fv(shdr_pos, p0); + immVertex2fv(shdr_pos, p0 + p_vec * distance); + immEnd(); + } + } + } + + /* Draw curve end radii. */ + if (fill_op.use_gap_close_radius) { + + /* Draw connected curve ends. */ + immUniformColor3fv(fill_op.gap_closed_color); + for (const int2 &connection : fill_op.radius_connections) { + const int point_i0 = fill_op.get_curve_point_by_end_index(connection[0]); + const int point_i1 = fill_op.get_curve_point_by_end_index(connection[1]); + immBegin(GPU_PRIM_LINES, 2); + immVertex2fv(shdr_pos, fill_op.curves_2d.points_2d[point_i0]); + immVertex2fv(shdr_pos, fill_op.curves_2d.points_2d[point_i1]); + immEnd(); + } + + /* Draw unconnected curve end radii. */ + immUniformColor3fv(fill_op.gap_closure_color); + GPU_line_width(2.0f); + + int curve_end_index = -1; + for (const bool connected : fill_op.connected_by_radius) { + curve_end_index++; + if (connected) { + continue; + } + + /* Skip ends of cyclic curves. */ + const int curve_i = int(curve_end_index / 2); + if (fill_op.curves_2d.is_cyclic[curve_i]) { + continue; + } + + /* Draw radius. */ + const int point_i = fill_op.get_curve_point_by_end_index(curve_end_index); + imm_draw_circle_wire_2d(shdr_pos, + fill_op.curves_2d.points_2d[point_i][0], + fill_op.curves_2d.points_2d[point_i][1], + fill_op.gap_distance, + 40); + } + } + + immUnbindProgram(); + + GPU_line_width(1.0f); + GPU_line_smooth(false); + GPU_blend(GPU_BLEND_NONE); + +#ifdef GP_FILL_DEBUG_MODE + fill_op.debug_draw_curve_indices(); +#endif +} + +/** + * Clean up the fill operator data. + */ +static void fill_exit(bContext *C, wmOperator *op) +{ + WM_cursor_modal_restore(CTX_wm_window(C)); + + if (op->customdata == nullptr) { + return; + } + + const ToolSettings *ts = CTX_data_tool_settings(C); + Brush *brush = BKE_paint_brush(&ts->gp_paint->paint); + switch (brush->gpencil_settings->fill_mode) { + case GP_FILL_MODE_FLOOD: + flood_fill_exit(*op); + break; + case GP_FILL_MODE_GEOMETRY: + geometry_fill_exit(*op); + break; + } + + op->customdata = nullptr; +} + +/** + * Modal handler for the fill operator: + * - Change gap closure length/radius + * - Perform the fill at second mouse click + */ +static int fill_modal(bContext *C, wmOperator *op, const wmEvent *event) +{ + int modal_state = OPERATOR_RUNNING_MODAL; + + const ToolSettings *ts = CTX_data_tool_settings(C); + Brush *brush = BKE_paint_brush(&ts->gp_paint->paint); + switch (brush->gpencil_settings->fill_mode) { + case GP_FILL_MODE_FLOOD: + modal_state = flood_fill_modal(*op, *event); + break; + case GP_FILL_MODE_GEOMETRY: + modal_state = geometry_fill_modal(*op, *event); + break; + } + + switch (modal_state) { + case OPERATOR_FINISHED: + fill_exit(C, op); + WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, nullptr); + break; + case OPERATOR_CANCELLED: + fill_exit(C, op); + break; + } + + return modal_state; +} + +/** + * Invoke the fill operator at first mouse click in the viewport. + */ +static int fill_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/) +{ + const Scene *scene = CTX_data_scene(C); + const Object *object = CTX_data_active_object(C); + const GreasePencil &grease_pencil = *static_cast(object->data); + + /* Check active layer. */ + if (!grease_pencil.has_active_layer()) { + BKE_report(op->reports, RPT_ERROR, "No active Grease Pencil layer"); + return OPERATOR_CANCELLED; + } + + /* Check if layer is editable. */ + const bke::greasepencil::Layer *active_layer = grease_pencil.get_active_layer(); + if (!active_layer->is_editable()) { + BKE_report(op->reports, RPT_ERROR, "Grease Pencil layer is not editable"); + return OPERATOR_CANCELLED; + } + + /* TODO: Check active frame. */ + if (!grease_pencil.get_active_layer()->frames().contains(scene->r.cfra)) { + if (!blender::animrig::is_autokey_on(scene)) { + BKE_report(op->reports, RPT_ERROR, "No Grease Pencil frame to draw on"); + return OPERATOR_CANCELLED; + } + } + + /* Init tool data. */ + bool success = false; + const ToolSettings *ts = CTX_data_tool_settings(C); + Brush *brush = BKE_paint_brush(&ts->gp_paint->paint); + switch (brush->gpencil_settings->fill_mode) { + case GP_FILL_MODE_FLOOD: + success = flood_fill_invoke(C, op); + break; + case GP_FILL_MODE_GEOMETRY: + success = geometry_fill_invoke(C, op); + break; + } + + if (!success) { + fill_exit(C, op); + BKE_report( + op->reports, + RPT_ERROR, + "No Grease Pencil layers with edge strokes found, see 'Layers' in Advanced options"); + return OPERATOR_CANCELLED; + } + + /* Add modal handler. */ + WM_event_add_modal_handler(C, op); + + /* Set cursor. */ + WM_cursor_modal_set(CTX_wm_window(C), WM_CURSOR_PAINT_BRUSH); + + return OPERATOR_RUNNING_MODAL; +} + +/** + * Definition of the fill operator. + */ +static void GREASE_PENCIL_OT_fill(wmOperatorType *ot) +{ + ot->name = "Fill"; + ot->idname = __func__; + ot->description = "Fill a shape formed by strokes"; + + ot->poll = grease_pencil_painting_fill_poll; + ot->invoke = fill_invoke; + ot->modal = fill_modal; + ot->cancel = fill_exit; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; +} + +/** \} */ + +} // namespace blender::ed::greasepencil::fill + +void ED_operatortypes_grease_pencil_fill() +{ + using namespace blender::ed::greasepencil::fill; + + WM_operatortype_append(GREASE_PENCIL_OT_fill); +} diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_fill.hh b/source/blender/editors/grease_pencil/intern/grease_pencil_fill.hh new file mode 100644 index 00000000000..c4f9869c3e8 --- /dev/null +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_fill.hh @@ -0,0 +1,1023 @@ +/* SPDX-FileCopyrightText: 2023 Blender Foundation + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edgreasepencil + */ + +#pragma once + +/* DEBUG: show developer extras in viewport and console. */ +/* #define GP_FILL_DEBUG_MODE */ + +#include "BKE_context.hh" +#include "BKE_grease_pencil.hh" +#include "BKE_material.h" +#include "BKE_paint.hh" +#include "BKE_report.hh" + +#include "BLI_kdtree.h" +#include "BLI_threads.h" + +#include "ANIM_keyframing.hh" + +#include "DEG_depsgraph_query.hh" + +#include "ED_curves.hh" +#include "ED_grease_pencil.hh" +#include "ED_screen.hh" +#include "ED_space_api.hh" +#include "ED_view3d.hh" + +#include "GPU_framebuffer.h" +#include "GPU_immediate.h" +#include "GPU_matrix.h" +#include "GPU_state.h" + +#include "WM_api.hh" + +#include "grease_pencil_fill_flood.hh" +#include "grease_pencil_fill_geometry.hh" + +#ifdef GP_FILL_DEBUG_MODE +# include "BLF_api.hh" +# include "UI_interface.hh" +# include "UI_resources.hh" +#endif + +namespace blender::ed::greasepencil::fill { + +void draw_overlay(const bContext * /*C*/, ARegion *region, void *arg); + +/** + * Operator handles for flood based fill. + */ +void flood_fill_exit(const wmOperator &op); +bool flood_fill_exec(const wmOperator &op); +int flood_fill_modal(const wmOperator &op, const wmEvent &event); +bool flood_fill_invoke(bContext *C, wmOperator *op); + +/** + * Operator handles for geometry based fill. + */ +void geometry_fill_exit(const wmOperator &op); +bool geometry_fill_exec(const wmOperator &op); +int geometry_fill_modal(const wmOperator &op, const wmEvent &event); +bool geometry_fill_invoke(bContext *C, wmOperator *op); + +/* Number of nearest neighbours when looking for gap closure by curve end radius. */ +static constexpr unsigned int RADIUS_NEAREST_NUM = 12; +/* Number of pixels the gap closure size stands for. */ +static constexpr float GAP_PIXEL_FACTOR = 25.0f; +/* Margin for intersection distances to be considered equal. */ +static constexpr float DISTANCE_EPSILON = 0.001f; +static constexpr float DISTANCE_SORT_EPSILON = 0.02f; +/* Margin for vector cross product considered to be parallel. */ +static constexpr float PARALLEL_EPSILON = 0.01f; + +/* Base fill operation class, with data members and methods for gap closure and conversion of + * curves to 2D screen space. */ +class FillOperation { + public: + /* Active Grease Pencil object. */ + GreasePencil *grease_pencil; + /* View context data.*/ + ViewContext vc; + /* Fill brush. */ + Brush *brush; + /* Additive drawing (from tool settings). */ + bool additive_drawing; + + /* For modal event handling: wait for key release before processing the key event again. */ + bool wait_for_release{}; + /* Wait-for-release event key. */ + short wait_event_type{}; + + /* Mouse position of the fill operation click. */ + float2 mouse_pos{}; + + /* Frame number to perform the fill operation on. */ + int frame_number; + + /* True when edge gaps are closed by extending the curve ends. */ + bool use_gap_close_extend{}; + /* True when edge gaps are closed by curve end radii. */ + bool use_gap_close_radius{}; + /* True when curve end extensions stop at their first intersection. */ + bool extensions_stop_at_first_intersection{}; + /* Gap closure distance in pixels. */ + float gap_distance{}; + /* True when edge gaps are closed by curve proximity. */ + bool use_gap_close_proximity{}; + /* Curve proximity distance in pixels. */ + float proximity_distance; + float proximity_distance_squared; + + /* Colors of gap closure visual aids. */ + float gap_closure_color[3]; + float gap_closed_color[3]; + float gap_proximity_color[4]; + + /* Draw handle for 3D viewport overlay. */ + void *draw_handle = nullptr; + + /* Curve points converted to viewport 2D space. */ + Curves2DSpace curves_2d{}; + /* Curve end extensions in viewport 2D space. */ + Curves2DSpace extensions_2d{}; + /* Flag for curve end extensions, true when the extension intersects a curve or other + * end extension. */ + Array extension_has_intersection; + /* Intersections of curve end extensions with curves or other extensions. */ + Vector extension_intersections; + /* Slight normalized extension of all curve segments (epsilon), used to find intersections at the + * outer ends of the curve segments. */ + Array curve_segment_epsilon; + /* Ratio between length of extension and length of curve end segment. */ + Array extension_length_ratio; + /* KD tree of curve ends in 2D space, used for gap closure by radius. */ + KDTree_2d *curve_ends = nullptr; + /* Flag indicating a curve end is connected by radius with one or more other curve ends. */ + Array connected_by_radius; + /* Curve end connections (by radius) with other curve ends. */ + Vector radius_connections; + + /** + * Overridable methods, implemented by #FloodFillOperation and #GeometryFillOperation. + */ + virtual void store_overlapping_segment( + const int, const int, const bool, const int, const int) = 0; + virtual bool execute_fill() = 0; + + /** + * Get the normalized distance from v1 and v3 to the intersection point of line segments v1-v2 + * and v3-v4. + */ + float2 get_intersection_distance_normalized(const float2 &v1, + const float2 &v2, + const float2 &v3, + const float2 &v4) + { + float2 distance = {0.0f, 0.0f}; + + /* Get intersection point. */ + const float a1 = v2[1] - v1[1]; + const float b1 = v1[0] - v2[0]; + const float c1 = a1 * v1[0] + b1 * v1[1]; + + const float a2 = v4[1] - v3[1]; + const float b2 = v3[0] - v4[0]; + const float c2 = a2 * v3[0] + b2 * v3[1]; + + const float det = a1 * b2 - a2 * b1; + BLI_assert(det != 0.0f); + + float2 isect; + isect[0] = (b2 * c1 - b1 * c2) / det; + isect[1] = (a1 * c2 - a2 * c1) / det; + + /* Get normalized distance from v1 and v3 to intersection point. */ + const float v1_length = math::length(v2 - v1); + const float v3_length = math::length(v4 - v3); + + distance[0] = (v1_length == 0.0f ? + 0.0f : + math::clamp(math::length(isect - v1) / v1_length, 0.0f, 1.0f)); + distance[1] = (v3_length == 0.0f ? + 0.0f : + math::clamp(math::length(isect - v3) / v3_length, 0.0f, 1.0f)); + return distance; + } + + /** + * Get all intersections of a segment with other curves. + */ + Vector get_intersections_of_segment_with_curves( + const float2 &segment_a, + const float2 &segment_b, + const int segment_curve_index, + const Curves2DSpace &curves_2d, + const float2 &adj_a = {FLT_MAX, FLT_MAX}, + const float2 &adj_b = {FLT_MAX, FLT_MAX}, + const bool store_overlap = false, + const int segment_point_a = 0, + const int segment_point_b = 0, + const int segment_direction = 1, + const bool use_epsilon = false) + { + /* Inits. */ + Vector intersections; + ThreadMutex mutex = BLI_MUTEX_INITIALIZER; + + const float2 segment_vec = segment_b - segment_a; + + float2 segment_epsilon = {0.0f, 0.0f}; + if (use_epsilon) { + if (segment_direction == 1) { + const int epsilon_i = curves_2d.point_offset[segment_curve_index] + segment_point_a; + segment_epsilon = -this->curve_segment_epsilon[epsilon_i]; + } + else { + const int epsilon_i = curves_2d.point_offset[segment_curve_index] + segment_point_b; + segment_epsilon = this->curve_segment_epsilon[epsilon_i]; + } + } + + /* Loop all curves, looking for intersecting segments. */ + threading::parallel_for( + curves_2d.point_offset.index_range(), 256, [&](const IndexRange range) { + rctf bbox_segment, bbox_isect; + + for (const int curve_i : range) { + /* Only process curves with at least two points. */ + if (curves_2d.point_size[curve_i] < 2) { + continue; + } + + /* Create a slightly extended segment. Otherwise an intersection can be missed when it + * is on the outer end of a segment. */ + float2 seg_a_extended = segment_a; + float2 seg_b_extended = segment_b; + if (use_epsilon && curve_i != segment_curve_index) { + seg_a_extended += segment_epsilon; + seg_b_extended -= segment_epsilon; + } + + /* Create bounding box around the segment. */ + BLI_rctf_init_minmax(&bbox_segment); + BLI_rctf_do_minmax_v(&bbox_segment, seg_a_extended); + BLI_rctf_do_minmax_v(&bbox_segment, seg_b_extended); + + /* Do a quick bounding box check. When the bounding box of a curve doesn't + * intersect with the segment, none of the curve segments do. */ + if (!BLI_rctf_isect(&bbox_segment, &curves_2d.curve_bbox[curve_i], nullptr)) { + continue; + } + + /* Get point range. */ + const int point_offset = curves_2d.point_offset[curve_i]; + const int point_size = curves_2d.point_size[curve_i] - + (curves_2d.is_cyclic[curve_i] ? 0 : 1); + const int point_last = point_offset + curves_2d.point_size[curve_i] - 1; + + /* Skip curves with identical (overlapping) segments, because they produce + * false-positive intersections. Overlapping curves are most likely created by a + * previous fill operation. */ + if (curve_i != segment_curve_index) { + bool skip_curve = false; + for (const int point_i : IndexRange(point_offset, point_size)) { + const int point_i_next = (point_i == point_last ? point_offset : point_i + 1); + const float2 p0 = curves_2d.points_2d[point_i]; + const float2 p1 = curves_2d.points_2d[point_i_next]; + + /* Check for identical segments. */ + if (segment_a == p0 && segment_b == p1) { + if (store_overlap) { + store_overlapping_segment( + curve_i, point_i - point_offset, 1, segment_point_a, segment_direction); + } + skip_curve = true; + break; + } + if (segment_a == p1 && segment_b == p0) { + if (store_overlap) { + store_overlapping_segment( + curve_i, point_i - point_offset, -1, segment_point_a, segment_direction); + } + skip_curve = true; + break; + } + } + if (skip_curve) { + continue; + } + } + + /* Find intersecting curve segments. */ + int prev_intersection_point = -1; + for (const int point_i : IndexRange(point_offset, point_size)) { + const int point_i_next = (point_i == point_last ? point_offset : point_i + 1); + const float2 p0 = curves_2d.points_2d[point_i]; + const float2 p1 = curves_2d.points_2d[point_i_next]; + float2 p0_extended = p0; + float2 p1_extended = p1; + if (use_epsilon && curve_i != segment_curve_index) { + p0_extended -= this->curve_segment_epsilon[point_i]; + p1_extended += this->curve_segment_epsilon[point_i]; + } + + /* Skip when previous segment was intersecting. */ + if (prev_intersection_point == point_i) { + continue; + } + + /* Skip when bounding boxes don't overlap. */ + BLI_rctf_init( + &bbox_isect, p0_extended[0], p0_extended[0], p0_extended[1], p0_extended[1]); + BLI_rctf_do_minmax_v(&bbox_isect, p1_extended); + if (!BLI_rctf_isect(&bbox_segment, &bbox_isect, nullptr)) { + continue; + } + + /* Don't self check. */ + if (curve_i == segment_curve_index) { + if (segment_a == p0 || segment_a == p1 || segment_b == p0 || segment_b == p1) { + continue; + } + } + + /* Skip identical adjacent segments. */ + if ((adj_a == p0 && segment_a == p1) || (segment_b == p0 && adj_b == p1) || + (adj_b == p0 && segment_b == p1) || (segment_a == p0 && adj_a == p1)) + { + continue; + } + + /* Skip segments that exactly overlap the current one. */ + if (segment_a == p0 || segment_a == p1 || segment_b == p0 || segment_b == p1) { + if (compare_ff(cross_v2v2(p1 - p0, segment_vec), 0.0f, PARALLEL_EPSILON)) { + continue; + } + } + + /* Check for intersection. */ + auto isect = math::isect_seg_seg( + seg_a_extended, seg_b_extended, p0_extended, p1_extended); + if (ELEM(isect.kind, isect.LINE_LINE_CROSS, isect.LINE_LINE_EXACT)) { + IntersectingCurve intersection{}; + intersection.curve_index_2d = curve_i; + intersection.point_start = point_i - point_offset; + intersection.point_end = point_i_next - point_offset; + intersection.distance = get_intersection_distance_normalized( + segment_a, segment_b, p0, p1); + intersection.has_stroke = curves_2d.has_stroke[curve_i]; + + BLI_mutex_lock(&mutex); + intersections.append(intersection); + BLI_mutex_unlock(&mutex); + + prev_intersection_point = point_i_next; + } + } + } + }); + + return intersections; + } + + /** + * Create a KD tree for the gap closure of curve ends by radius. + */ + void init_curve_end_radii() + { + /* Create KD tree for all curve ends. */ + const int curve_num = this->curves_2d.point_offset.size(); + this->curve_ends = BLI_kdtree_2d_new(curve_num * 2); + + /* Add curve ends. */ + int tree_index = 0; + for (const int curve_i : this->curves_2d.point_offset.index_range()) { + /* Add first curve point. */ + int point_i = this->curves_2d.point_offset[curve_i]; + BLI_kdtree_2d_insert(this->curve_ends, tree_index, this->curves_2d.points_2d[point_i]); + tree_index++; + + /* Add last curve point. */ + point_i += this->curves_2d.point_size[curve_i] - 1; + BLI_kdtree_2d_insert(this->curve_ends, tree_index, this->curves_2d.points_2d[point_i]); + tree_index++; + } + + BLI_kdtree_2d_balance(this->curve_ends); + + /* Init array with curve end connection flag. */ + this->connected_by_radius = Array(curve_num * 2, false); + } + + /** + * Determine which curves ends are connected with each other based on radius. + */ + void get_connected_curve_end_radii() + { + const float max_dist = 2 * this->gap_distance; + + /* Init connections by radius. */ + this->connected_by_radius.fill(false); + this->radius_connections.clear(); + + /* Check all curvess. */ + for (const int curve_i : this->curves_2d.point_offset.index_range()) { + /* Skip cyclic curves. */ + if (this->curves_2d.is_cyclic[curve_i]) { + continue; + } + + /* Check both ends. */ + for (int side = 0; side <= 1; side++) { + const int kdtree_index = curve_i * 2 + side; + const int point_index = this->curves_2d.point_offset[curve_i] + + (side == 0 ? 0 : this->curves_2d.point_size[curve_i] - 1); + + /* Find nearest curve end points. */ + KDTreeNearest_2d nearest[RADIUS_NEAREST_NUM]; + const int nearest_num = BLI_kdtree_2d_find_nearest_n( + this->curve_ends, this->curves_2d.points_2d[point_index], nearest, RADIUS_NEAREST_NUM); + + for (int i = 0; i < nearest_num; i++) { + /* Skip self, already registered curve ends and curve ends out of close radius range. */ + if (nearest[i].index <= kdtree_index || nearest[i].dist > max_dist) { + continue; + } + + /* Skip cyclic curves. */ + const int nearest_curve = int(nearest[i].index / 2); + if (this->curves_2d.is_cyclic[nearest_curve]) { + continue; + } + + /* Flag connection and append to list for drawing. */ + this->connected_by_radius[kdtree_index] = true; + this->connected_by_radius[nearest[i].index] = true; + this->radius_connections.append({kdtree_index, nearest[i].index}); + } + } + } + } + + /** + * Init the data structure for curve end extensions. + */ + void init_curve_end_extensions() + { + const int curve_num = this->curves_2d.point_offset.size() * 2; + const int point_num = curve_num * 2; + + this->extensions_2d.point_offset = Array(curve_num); + this->extensions_2d.point_size = Array(curve_num, 2); + this->extensions_2d.is_cyclic = Array(curve_num, false); + this->extensions_2d.has_stroke = Array(curve_num, false); + this->extensions_2d.drawing_index_2d = Array(curve_num); + this->extensions_2d.points_2d = Array(point_num); + this->extensions_2d.curve_bbox = Array(curve_num); + this->extension_length_ratio = Array(curve_num); + this->extension_has_intersection = Array(curve_num, false); + } + + /** + * Add a curve end extension to the set of extensions. + */ + void add_curve_end_extension(const int curve_i, + const int curve_ext, + const int point_i, + const int next_point_delta, + const int point_i_ext) + { + /* Set drawing index and point index for extension. */ + this->extensions_2d.drawing_index_2d[curve_ext] = this->curves_2d.drawing_index_2d[curve_i]; + this->extensions_2d.point_offset[curve_ext] = point_i_ext; + this->extensions_2d.is_cyclic[curve_ext] = this->curves_2d.is_cyclic[curve_i]; + + /* Use the vector between the two outer points of the curve to calculate the extension + * coordinates. */ + const float2 co = this->curves_2d.points_2d[point_i]; + this->extensions_2d.points_2d[point_i_ext] = co; + float2 end_vec = co - this->curves_2d.points_2d[point_i + next_point_delta]; + const float end_length = math::length(end_vec); + end_vec = math::normalize(end_vec) * this->gap_distance; + const float extension_length = math::length(end_vec); + this->extensions_2d.points_2d[point_i_ext + 1] = co + end_vec; + this->extension_length_ratio[curve_ext] = (end_length == 0.0f ? 0.0f : + extension_length / end_length); + } + + /** + * Create a set of 2D curve end extensions. Check if the extensions intersect with curves or + * other end extensions. + */ + void get_curve_end_extensions() + { + /* Create extension curves. */ + for (const int curve_i : this->curves_2d.point_offset.index_range()) { + /* Two extensions for each curve. */ + const int curve_ext = curve_i * 2; + /* Each extension contains two points. */ + const int point_i_ext = curve_ext * 2; + const int point_size = this->curves_2d.point_size[curve_i]; + + /* Create extension for the first segment of the 2D curve. */ + int point_i = this->curves_2d.point_offset[curve_i]; + int next_point_delta = (point_size > 1 ? 1 : 0); + this->add_curve_end_extension(curve_i, curve_ext, point_i, next_point_delta, point_i_ext); + + /* Create extension for the last segment of the 2D curve. */ + point_i += point_size - 1; + next_point_delta = (point_size > 1 ? -1 : 0); + this->add_curve_end_extension( + curve_i, curve_ext + 1, point_i, next_point_delta, point_i_ext + 2); + + /* Create bounding boxes for the extensions. */ + BLI_rctf_init_minmax(&this->extensions_2d.curve_bbox[curve_ext]); + BLI_rctf_do_minmax_v(&this->extensions_2d.curve_bbox[curve_ext], + this->extensions_2d.points_2d[point_i_ext]); + BLI_rctf_do_minmax_v(&this->extensions_2d.curve_bbox[curve_ext], + this->extensions_2d.points_2d[point_i_ext + 1]); + + BLI_rctf_init_minmax(&this->extensions_2d.curve_bbox[curve_ext + 1]); + BLI_rctf_do_minmax_v(&this->extensions_2d.curve_bbox[curve_ext + 1], + this->extensions_2d.points_2d[point_i_ext + 2]); + BLI_rctf_do_minmax_v(&this->extensions_2d.curve_bbox[curve_ext + 1], + this->extensions_2d.points_2d[point_i_ext + 3]); + } + + /* Clear intersection data. */ + this->extension_intersections.clear(); + this->extension_has_intersection.fill(false); + + /* Check intersections of extensions with curves or other end extensions. */ + for (const int extension_index : this->extensions_2d.point_offset.index_range()) { + /* Skip end extensions of cyclic curves. */ + if (this->extensions_2d.is_cyclic[extension_index]) { + continue; + } + + const int point_offset = this->extensions_2d.point_offset[extension_index]; + const float2 segment_point_a = this->extensions_2d.points_2d[point_offset]; + const float2 segment_point_b = this->extensions_2d.points_2d[point_offset + 1]; + + /* Get intersections with other curve end extensions. */ + Vector all_intersections; + Vector intersections = this->get_intersections_of_segment_with_curves( + segment_point_a, segment_point_b, extension_index, this->extensions_2d); + + for (auto intersection : intersections) { + intersection.with_end_extension = true; + intersection.extension_index = extension_index; + all_intersections.append(intersection); + } + + /* Get intersections with other curves. */ + const int curve_i = int(extension_index / 2); + intersections = this->get_intersections_of_segment_with_curves( + segment_point_a, segment_point_b, curve_i, this->curves_2d); + + for (auto intersection : intersections) { + intersection.with_end_extension = false; + intersection.extension_index = extension_index; + all_intersections.append(intersection); + } + + /* Sort intersections on distance. */ + if (!all_intersections.is_empty()) { + std::sort(all_intersections.begin(), + all_intersections.end(), + [](const IntersectingCurve &a, const IntersectingCurve &b) { + return a.distance[0] < b.distance[0]; + }); + + /* Add closest result(s) to list of extension intersections. */ + const float shortest_dist = all_intersections.first().distance[0]; + for (const auto &intersection : all_intersections) { + if (this->extensions_stop_at_first_intersection && + (intersection.distance[0] - shortest_dist) > DISTANCE_SORT_EPSILON) + { + break; + } + this->extension_intersections.append(intersection); + this->extension_has_intersection[intersection.extension_index] = true; + } + } + } + } + + /** + * Get a slight extension for each curve segment, based on the normalized vector of each + * curve point to the next. + */ + void get_curve_segment_epsilons() + { + this->curve_segment_epsilon = Array(this->curves_2d.points_2d.size()); + + threading::parallel_for( + this->curves_2d.point_offset.index_range(), 256, [&](const IndexRange range) { + for (const int curve_i : range) { + const int point_last = this->curves_2d.point_offset[curve_i] + + this->curves_2d.point_size[curve_i] - 1; + for (const int point_i : IndexRange(this->curves_2d.point_offset[curve_i], + this->curves_2d.point_size[curve_i])) + { + const int point_next = (point_i == point_last ? + this->curves_2d.point_offset[curve_i] : + point_i + 1); + this->curve_segment_epsilon[point_i] = math::normalize( + this->curves_2d.points_2d[point_next] - + this->curves_2d.points_2d[point_i]) * + DISTANCE_EPSILON; + } + } + }); + } + +#ifdef GP_FILL_DEBUG_MODE + /** + * Debug function: show curve indices in the viewport. + */ + void debug_draw_curve_indices() + { + const int font_id = BLF_default(); + const uiStyle *style = UI_style_get(); + BLF_size(font_id, style->widget.points * UI_SCALE_FAC * 0.8); + + /* Draw point indices. */ + BLF_color4fv(font_id, float4{0.0f, 0.0f, 0.0f, 0.5f}); + for (const int curve_i : this->curves_2d.point_offset.index_range()) { + const int point_offset = this->curves_2d.point_offset[curve_i]; + const int step = 5; + float x_prev = FLT_MAX; + float y_prev = FLT_MAX; + for (int point_i = step; point_i < this->curves_2d.point_size[curve_i]; point_i += step) { + const std::string str = std::to_string(point_i) + "-" + std::to_string(curve_i); + const char *text = str.c_str(); + + const float x = this->curves_2d.points_2d[point_offset + point_i][0] - strlen(text) * 5.4; + const float y = this->curves_2d.points_2d[point_offset + point_i][1] + 1; + + if (((x - x_prev) * (x - x_prev) + (y - y_prev) * (y - y_prev)) > 1000) { + BLF_position(font_id, x, y, 0); + BLF_draw(font_id, text, strlen(text)); + + x_prev = x; + y_prev = y; + } + } + } + + /* Draw curve indices. */ + BLF_size(font_id, style->widget.points * UI_SCALE_FAC * 1.2); + BLF_color4fv(font_id, float4{1.0f, 0.0f, 0.2f, 1.0f}); + BLF_enable(font_id, BLF_BOLD); + + for (const int curve_i : this->curves_2d.point_offset.index_range()) { + const std::string str = std::to_string(curve_i); + const char *text = str.c_str(); + + const int point_offset = this->curves_2d.point_offset[curve_i]; + const float x = this->curves_2d.points_2d[point_offset][0] - strlen(text) * 10; + const float y = this->curves_2d.points_2d[point_offset][1] + 2; + BLF_position(font_id, x, y, 0); + + BLF_draw(font_id, text, strlen(text)); + } + + BLF_disable(font_id, BLF_BOLD); + } +#endif + + /** + * Get curve point index given an end extension index. + */ + int get_curve_point_by_end_index(const int end_index) + { + const int curve_i = int(end_index / 2); + int point_i = this->curves_2d.point_offset[curve_i]; + if ((end_index & 1) == 1) { + point_i += this->curves_2d.point_size[curve_i] - 1; + } + return point_i; + } + + /** + * Get latest tool settings before executing the actual fill. + */ + void get_latest_toolsettings() + { + const bool use_gap_closing = (this->brush->gpencil_settings->fill_extend_fac > FLT_EPSILON); + this->use_gap_close_extend = (this->brush->gpencil_settings->fill_extend_mode == + GP_FILL_EMODE_EXTEND) && + use_gap_closing; + this->use_gap_close_radius = (this->brush->gpencil_settings->fill_extend_mode == + GP_FILL_EMODE_RADIUS) && + use_gap_closing; + this->gap_distance = this->brush->gpencil_settings->fill_extend_fac * GAP_PIXEL_FACTOR; + this->use_gap_close_proximity = (this->brush->gpencil_settings->fill_proximity_distance > 0); + this->proximity_distance = float(this->brush->gpencil_settings->fill_proximity_distance); + this->proximity_distance_squared = this->proximity_distance * this->proximity_distance; + } + + /** + * Update the gap closure By Proximity in the viewport when the value changed. + */ + void update_proximity_distance(const int delta) + { + this->brush->gpencil_settings->fill_proximity_distance = math::max( + 0, math::min(100, this->brush->gpencil_settings->fill_proximity_distance + delta)); + this->get_latest_toolsettings(); + ED_region_tag_redraw(this->vc.region); + } + + /** + * Update the gap closure connections when the radius changed. + */ + void update_gap_distance(const float delta) + { + this->brush->gpencil_settings->fill_extend_fac = math::max( + 0.0f, math::min(10.0f, this->brush->gpencil_settings->fill_extend_fac + delta)); + this->get_latest_toolsettings(); + + if (this->use_gap_close_extend) { + this->get_curve_end_extensions(); + } + if (this->use_gap_close_radius) { + this->get_connected_curve_end_radii(); + } + ED_region_tag_redraw(this->vc.region); + } + + /** + * Get a list of layers used for the fill edge detection. The list is based on the 'Layers' field + * in the tool settings. + */ + std::pair, Vector> get_fill_edge_layers( + const int frame_number) + { + /* Find index of active layer. */ + int active_layer_index = -1; + const bke::greasepencil::Layer *active_layer = this->grease_pencil->get_active_layer(); + Span layers = this->grease_pencil->layers(); + for (int layer_index = 0; layer_index < layers.size(); layer_index++) { + if (layers[layer_index] == active_layer) { + active_layer_index = layer_index; + break; + } + } + + /* Select layers based on position in the layer collection. */ + Vector drawings; + Vector layer_indices; + for (int layer_index = 0; layer_index < layers.size(); layer_index++) { + /* Skip invisible layers. */ + if (!layers[layer_index]->is_visible()) { + continue; + } + + bool add = false; + switch (this->brush->gpencil_settings->fill_layer_mode) { + case GP_FILL_GPLMODE_ACTIVE: + add = (layer_index == active_layer_index); + break; + case GP_FILL_GPLMODE_ABOVE: + add = (layer_index == active_layer_index + 1); + break; + case GP_FILL_GPLMODE_BELOW: + add = (layer_index == active_layer_index - 1); + break; + case GP_FILL_GPLMODE_ALL_ABOVE: + add = (layer_index > active_layer_index); + break; + case GP_FILL_GPLMODE_ALL_BELOW: + add = (layer_index < active_layer_index); + break; + case GP_FILL_GPLMODE_VISIBLE: + add = true; + break; + } + + if (!add) { + continue; + } + if (const bke::greasepencil::Drawing *drawing = this->grease_pencil->get_editable_drawing_at( + *layers[layer_index], frame_number)) + { + drawings.append(drawing); + layer_indices.append(layer_index); + } + } + + return {drawings, layer_indices}; + } + + /** + * Get drawings and gap closure data for a given frame number. + */ + bool get_drawings_and_gap_closures_at_frame(const int frame_number) + { + /* Get layers according to tool settings (visible, above, below, etc.) */ + Vector drawings; + Vector layer_indices; + std::tie(drawings, layer_indices) = this->get_fill_edge_layers(frame_number); + + if (drawings.is_empty()) { + return false; + } + + /* Convert curves to viewport 2D space. */ + this->curves_2d = curves_in_2d_space_get(*this->vc.obact, + this->vc, + *this->grease_pencil, + drawings, + layer_indices, + frame_number, + true); + + /* Calculate epsilon values of all 2D curve segments, used to avoid floating point precision + * errors. */ + this->get_curve_segment_epsilons(); + + /* When using extensions of the curve ends to close gaps, build an array of those + * two-point 'curves'. */ + if (this->use_gap_close_extend) { + this->init_curve_end_extensions(); + this->get_curve_end_extensions(); + } + + /* When using radii to close gaps, build KD tree of curve end points. */ + if (this->use_gap_close_radius) { + this->init_curve_end_radii(); + this->get_connected_curve_end_radii(); + } + + return true; + } + + /** + * Initialize the fill operator data. + */ + bool fill_init(bContext *C) + { + /* Get view context. */ + Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); + this->vc = ED_view3d_viewcontext_init(C, depsgraph); + + /* Get active GP object. */ + this->grease_pencil = static_cast(this->vc.obact->data); + + /* Get tool brush. */ + const ToolSettings *ts = CTX_data_tool_settings(C); + this->brush = BKE_paint_brush(&ts->gp_paint->paint); + this->additive_drawing = ((ts->gpencil_flags & GP_TOOL_FLAG_RETAIN_LAST) != 0); + + /* Init geometry fill flags. */ + this->use_gap_close_extend = (this->brush->gpencil_settings->fill_extend_mode == + GP_FILL_EMODE_EXTEND); + this->use_gap_close_radius = (this->brush->gpencil_settings->fill_extend_mode == + GP_FILL_EMODE_RADIUS); + this->extensions_stop_at_first_intersection = (this->brush->gpencil_settings->flag & + GP_BRUSH_FILL_STROKE_COLLIDE) != 0; + this->gap_distance = this->brush->gpencil_settings->fill_extend_fac * GAP_PIXEL_FACTOR; + this->wait_for_release = true; + this->wait_event_type = LEFTMOUSE; + this->use_gap_close_proximity = (this->brush->gpencil_settings->fill_proximity_distance > 0); + this->proximity_distance = float(this->brush->gpencil_settings->fill_proximity_distance); + this->proximity_distance_squared = this->proximity_distance * this->proximity_distance; + + const float default_gap_closure_color[3] = {1.0f, 0.0f, 0.5f}; + const float default_gap_closed_color[3] = {0.0f, 1.0f, 1.0f}; + const float default_gap_proximity_color[4] = {1.0f, 0.8f, 0.0f, 0.3f}; + copy_v3_v3(this->gap_closure_color, default_gap_closure_color); + copy_v3_v3(this->gap_closed_color, default_gap_closed_color); + copy_v4_v4(this->gap_proximity_color, default_gap_proximity_color); + + /* Get drawings for current frame. */ + if (!this->get_drawings_and_gap_closures_at_frame(this->vc.scene->r.cfra)) { + return false; + } + + /* Activate 3D viewport overlay for showing gap closure visual aids. */ + if ((this->brush->gpencil_settings->flag & GP_BRUSH_FILL_SHOW_EXTENDLINES) != 0) { + this->draw_handle = ED_region_draw_cb_activate( + this->vc.region->type, draw_overlay, this, REGION_DRAW_POST_PIXEL); + ED_region_tag_redraw(this->vc.region); + } + + return true; + } + + /** + * Clean up the fill operator data. + */ + void fill_exit() + { + /* Remove viewport overlay. */ + ED_region_draw_cb_exit(this->vc.region->type, this->draw_handle); + ED_region_tag_redraw(this->vc.region); + + /* Remove KD tree of curve ends. */ + if (this->curve_ends) { + BLI_kdtree_2d_free(this->curve_ends); + } + } + + /** + * Execute the fill operation (on second mouse click in the viewport). + */ + bool fill_exec() + { +#ifdef GP_FILL_DEBUG_MODE + /* DEBUG: measure time. */ + auto t1 = std::chrono::high_resolution_clock::now(); +#endif // GP_FILL_DEBUG_MODE + + this->get_latest_toolsettings(); + + /* Perform the fill operation for all selected keyframes. */ + const bool use_multi_frame_editing = (this->vc.scene->toolsettings->gpencil_flags & + GP_USE_MULTI_FRAME_EDITING) != 0; + const Array frame_numbers = get_frame_numbers_for_layer( + this->grease_pencil->get_active_layer()->wrap(), + this->vc.scene->r.cfra, + use_multi_frame_editing); + bool get_drawings = false; + bool success = false; + + for (const int frame_number : frame_numbers) { + /* For the current frame (first entry in the array) we already retrieved the drawings during + * the operator invoke, so no need to do it a second time. */ + if (get_drawings) { + if (!this->get_drawings_and_gap_closures_at_frame(frame_number)) { + continue; + } + } + get_drawings = true; + + this->frame_number = frame_number; + + if (execute_fill()) { + success = true; + } + } + +#ifdef GP_FILL_DEBUG_MODE + /* DEBUG: measure time. */ + auto t2 = std::chrono::high_resolution_clock::now(); + auto delta_t = std::chrono::duration_cast(t2 - t1); + printf("Fill operator took %d ms.\n", int(delta_t.count())); +#endif // GP_FILL_DEBUG_MODE + + return success; + } + + /** + * Modal handler for the fill operator: + * - Change gap closure length/radius + * - Perform the fill at second mouse click + */ + int fill_modal(const wmOperator &op, const wmEvent &event) + { + int modal_state = OPERATOR_RUNNING_MODAL; + + /* Prevent repeating event keys, when not released yet. */ + if (this->wait_for_release && this->wait_event_type == event.type) { + if (event.val == KM_RELEASE) { + this->wait_for_release = false; + } + return modal_state; + } + + switch (event.type) { + case EVT_ESCKEY: + case RIGHTMOUSE: + modal_state = OPERATOR_CANCELLED; + break; + + case EVT_PAGEUPKEY: + case WHEELUPMOUSE: + this->update_gap_distance((event.modifier & KM_SHIFT) ? 0.01f : 0.1f); + break; + case EVT_PAGEDOWNKEY: + case WHEELDOWNMOUSE: + this->update_gap_distance((event.modifier & KM_SHIFT) ? -0.01f : -0.1f); + break; + + case EVT_LEFTBRACKETKEY: + if (this->use_gap_close_proximity) { + this->update_proximity_distance(-1); + } + break; + case EVT_RIGHTBRACKETKEY: + if (this->use_gap_close_proximity) { + this->update_proximity_distance(1); + } + break; + + case LEFTMOUSE: { + /* Get mouse position of second click (the 'fill' click). */ + this->mouse_pos[0] = float(event.mval[0]); + this->mouse_pos[1] = float(event.mval[1]); + + /* Perform the fill operation. */ + if (this->fill_exec()) { + modal_state = OPERATOR_FINISHED; + } + else { + BKE_report(op.reports, RPT_INFO, "Unable to fill unclosed area"); + modal_state = OPERATOR_CANCELLED; + } + break; + } + } + + return modal_state; + } +}; + +} // namespace blender::ed::greasepencil::fill diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_fill_flood.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_fill_flood.cc new file mode 100644 index 00000000000..bf320efd961 --- /dev/null +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_fill_flood.cc @@ -0,0 +1,56 @@ +/* SPDX-FileCopyrightText: 2023 Blender Foundation + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edgreasepencil + */ + +#include "grease_pencil_fill.hh" + +namespace blender::ed::greasepencil::fill { + +class FloodFillOperation : public FillOperation { + public: + /* Unused in flood fill. */ + void store_overlapping_segment(const int, const int, const bool, const int, const int) override + { + } + + bool execute_fill() override + { + /* TODO... */ + printf("Perform flood fill algorithm on %lld strokes at frame %d \n", + this->curves_2d.point_offset.size(), + this->frame_number); + return true; + } +}; + +void flood_fill_exit(const wmOperator &op) +{ + FloodFillOperation *fill_op = static_cast(op.customdata); + fill_op->fill_exit(); + MEM_delete(fill_op); +} + +bool flood_fill_exec(const wmOperator &op) +{ + FloodFillOperation &fill_op = *static_cast(op.customdata); + return fill_op.fill_exec(); +} + +int flood_fill_modal(const wmOperator &op, const wmEvent &event) +{ + FloodFillOperation *fill_op = static_cast(op.customdata); + return fill_op->fill_modal(op, event); +} + +bool flood_fill_invoke(bContext *C, wmOperator *op) +{ + FloodFillOperation *fill_op = MEM_new(__func__); + op->customdata = fill_op; + return fill_op->fill_init(C); +} + +} // namespace blender::ed::greasepencil::fill diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_fill_flood.hh b/source/blender/editors/grease_pencil/intern/grease_pencil_fill_flood.hh new file mode 100644 index 00000000000..2df2dc00cee --- /dev/null +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_fill_flood.hh @@ -0,0 +1,13 @@ +/* SPDX-FileCopyrightText: 2023 Blender Foundation + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edgreasepencil + */ + +#pragma once + +namespace blender::ed::greasepencil::fill { + +} // namespace blender::ed::greasepencil::fill diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_fill_geometry.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_fill_geometry.cc new file mode 100644 index 00000000000..087c0779dcf --- /dev/null +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_fill_geometry.cc @@ -0,0 +1,2192 @@ +/* SPDX-FileCopyrightText: 2023 Blender Foundation + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edgreasepencil + */ + +#include "grease_pencil_fill.hh" + +/** + * Explanation of the used algorithm for geometry-based fill: + * + * + * | (C) segment 1 \ (D) + * -|-->>----------------------x--->>------------->>----\--- + * | \ + * ^ ^ \ segment 2 + * ^ | \ + * | (B) | \| + * | | |\ + * | (A) mouse | + * | click | + * | | segment 3 + * | | etc. + * | 1 | + * | | (E) | + * ^ 2 ---|----------<<----|- + * ^ /\ | + * | (G) / \ 3 | + * | e0 e1 / \ (F) | + * ..|....x-----x-----/------\---------<<----|- + * | r1 / \ + * r2 + * + * When the user clicks to fill an area, a ray (B) is casted from the mouse position (A) + * to find a first intersection point with a curve (C). + * From point (C) a 'walk-along-egde' search is carried out to find a fully closed fill edge. + * The walk is in clockwise direction (C -->>--). + * The edge is made up of segments. A segment is a point range in a curve. A segment ends at an + * intersection with another curve (D) or at the end of the curve itself (G). + * At an intersection (D), the right turn will be inspected first. Because we are inspecting the + * edge in clockwise direction, this guarantees us that we find the 'narrowest' edge. See for + * example at (F): by taking the right turn first, we find the smallest fill area (just as we + * should). At an intersection, there are three possible turns. When a turn is not leading to a + * closed edge (see e.g. (E), turn 1 and 2), the next turn is inspected. + * + * When a segment ends without intersection, the 'gap closure' inspection starts (G). In 'extend' + * mode, the extrapolated first/last two points of the curve (e0 and e1) are used to find + * intersections with other curves (or curve extensions). + * In 'radius' mode, we search for other curve ends (r2) within a radius of the end point (r1) + * of the curve. + * + * When a closed edge is found, we have to check if the mouse click position (A) is inside this + * edge. This is because edges can be self-intersecting and give a 'false-positive' close. + * + * All inspections are done with a 2D (viewport) representation of all curve points. + * When a closed edge is found, the fill geometry is created with the corresponding 3D + * coordinates of the segment points. That way we almost only use existing geometry points; at + * intersections we create one additional curve point, that's it. + * + * Finding intersections can be a bit troublesome when two curves overlap (sharing exact same + * curve points). This can easily occur when the fill tool is used twice: the original stroke + * and the fill curve share geometry points. Therefore, when looking for intersections, a check on + * overlapping segments is performed. And curves with a stroke material get priority over curves + * with only a fill material, since that is the most common situation: the outlines of the fill + * area are drawn with 'stroke' curves. + * + * In the code: + * #execute_fill() start of the algorithm, casting a ray (B) + * #find_closed_fill_edge() search for the narrowest closed edge + * #walk_along_curve() creating an edge segment, inspecting where the segment ends + */ + +namespace blender::ed::greasepencil::fill { + +class GeometryFillOperation : public FillOperation { + public: + /* Starting time of the geometry fill operator. */ + std::chrono::high_resolution_clock::time_point operator_time_start; + + /* The initial curve segments from which we try to find a closed fill edge. */ + Vector starting_segments; + /* The curve segments that will form a closed fill edge. */ + Vector segments; + /* Overlapping curve segments of the last created edge segment. */ + Vector overlapping_segments; + + /* The intersection distance of the start segment (found by the casted ray). */ + float start_distance; + + ThreadMutex mutex; + + /* ------------------------------------------------------------------------------ */ + /** \name Overlapping curve methods + * \{ */ + + /** + * Store a segment that shares points with an inspected edge segment. + */ + void store_overlapping_segment(const int overlapping_curve, + const int overlapping_point, + const bool in_opposite_dir, + const int base_point, + const int base_direction) override + { + const int overlapping_direction = (in_opposite_dir ? -base_direction : base_direction); + const bool overlapping_backwards = (overlapping_direction == -1); + const int overlapping_range_index = (overlapping_direction == 1 ? 0 : 1); + const int base_range_index = (base_direction == 1 ? 0 : 1); + + /* Look for an existing overlapping segment which point range we can expand. */ + for (OverlappingSegment &overlap : this->overlapping_segments) { + if (overlap.overlapping_curve_index == overlapping_curve && + overlap.overlapping_backwards == overlapping_backwards && + overlap.overlapping_point_range[overlapping_range_index] == + (overlapping_point - overlapping_direction) && + overlap.base_point_range[base_range_index] == (base_point - base_direction)) + { + overlap.overlapping_point_range[overlapping_range_index] = overlapping_point; + overlap.base_point_range[base_range_index] = base_point; + return; + } + } + + /* Create new overlapping segment. */ + OverlappingSegment overlap{}; + overlap.overlapping_curve_index = overlapping_curve; + overlap.overlapping_backwards = overlapping_backwards; + overlap.overlapping_point_range[0] = overlapping_point; + overlap.overlapping_point_range[1] = overlapping_point; + overlap.base_point_range[0] = base_point; + overlap.base_point_range[1] = base_point; + + BLI_mutex_lock(&this->mutex); + this->overlapping_segments.append(std::move(overlap)); + BLI_mutex_unlock(&this->mutex); + } + + /** \} */ + + /* ------------------------------------------------------------------------------ */ + /** \name Curve proximity methods + * \{ */ + + /** + * Check if the shortest distance between two line segments is within a given proximity distance. + * Note: this computation doesn't handle intersecting lines, but that is okay, since we check + * intersections already in a different method. + */ + std::tuple line_segments_are_in_proximity( + const float2 &segment_p0, + const float2 &segment_p1, + const float2 &curve_p0, + const float2 &curve_p1, + const float proximity_distance_squared) + { + constexpr float DET_EPSILON = 1e-6f; + + /* Calculate the shortest distance between the two line segments. */ + const float2 vec_segment = segment_p0 - segment_p1; + const float2 vec_curve = curve_p0 - curve_p1; + const float2 vec_segment_curve = segment_p1 - curve_p1; + + const float dot_seg = math::dot(vec_segment, vec_segment); + const float dot_seg_cur = math::dot(vec_segment, vec_curve); + const float dot_cur = math::dot(vec_curve, vec_curve); + const float dot_seg_seg_cur = math::dot(vec_segment, vec_segment_curve); + const float dot_cur_seg_cur = math::dot(vec_curve, vec_segment_curve); + + const float det = dot_seg * dot_cur - dot_seg_cur * dot_seg_cur; + float d_seg = det; + float d_cur = det; + float p_on_seg, p_on_cur; + + /* Abort when one of the segments has equal points. */ + if (dot_seg == 0.0f || dot_cur == 0.0f) { + return std::tuple(false, false, false); + } + + if (det < DET_EPSILON) { + /* Lines are almost parallel, force using point p0 on segment (to prevent division by zero + * later). */ + p_on_seg = 0.0f; + d_seg = 1.0f; + p_on_cur = dot_cur_seg_cur; + d_cur = dot_cur; + } + else { + /* Calculate points on the infinite lines. */ + p_on_seg = dot_seg_cur * dot_cur_seg_cur - dot_cur * dot_seg_seg_cur; + p_on_cur = dot_seg * dot_cur_seg_cur - dot_seg_cur * dot_seg_seg_cur; + + /* Limit to edges of segment line. */ + if (p_on_seg < 0.0f) { + p_on_seg = 0.0f; + p_on_cur = dot_cur_seg_cur; + d_cur = dot_cur; + } + else if (p_on_seg > d_seg) { + p_on_seg = d_seg; + p_on_cur = dot_cur_seg_cur + dot_seg_cur; + d_cur = dot_cur; + } + } + + /* Limit to edges of curve line. */ + if (p_on_cur < 0.0f) { + p_on_cur = 0.0f; + const float inv = -dot_seg_seg_cur; + if (inv < 0.0f) { + p_on_seg = 0.0f; + } + else if (inv > dot_seg) { + p_on_seg = d_seg; + } + else { + p_on_seg = inv; + d_seg = dot_seg; + } + } + else if (p_on_cur > d_cur) { + p_on_cur = d_cur; + const float inv = -dot_seg_seg_cur + dot_seg_cur; + if (inv < 0.0f) { + p_on_seg = 0.0f; + } + else if (inv > dot_seg) { + p_on_seg = d_seg; + } + else { + p_on_seg = inv; + d_seg = dot_seg; + } + } + + /* Get the difference between the two closest points. */ + const float c_seg = (math::abs(p_on_seg) < DET_EPSILON) ? 0.0f : p_on_seg / d_seg; + const float c_cur = (math::abs(p_on_cur) < DET_EPSILON) ? 0.0f : p_on_cur / d_cur; + const float dist_sq = math::length_squared(vec_segment_curve + c_seg * vec_segment - + c_cur * vec_curve); + + /* When the shortest distance between the line segments isn't within the proximity range, + * return false. */ + if (dist_sq > proximity_distance_squared) { + return std::tuple(false, false, false); + } + + /* When the nearest curve point is beyond segment point b, we want to use segment point b for + * the fill edge. */ + const bool use_segment_b = (c_seg == 0.0f); + + /* Establish which curve point is nearest to the segment start point. */ + const float dist_p0 = math::length_squared(curve_p0 - segment_p0); + const float dist_p1 = math::length_squared(curve_p1 - segment_p0); + const bool curve_p0_is_nearest = dist_p0 < dist_p1; + + return std::tuple(true, use_segment_b, curve_p0_is_nearest); + } + + /** + * Check if the outer end of an edge segment is in proximity of a curve head or tail. + */ + ProximityCurve get_segment_in_proximity_of_curve_head_tail(EdgeSegment &segment, + const int segment_direction, + const int curve_index) + { + /* Get segment end points. */ + const int segment_curve_index = segment.curve_index_2d; + const int segment_point_offset = this->curves_2d.point_offset[segment_curve_index]; + const int segment_point_end = (segment_direction == 1) ? segment.point_range[1] : + segment.point_range[0]; + const float2 segment_a = + this->curves_2d.points_2d[segment_point_offset + segment_point_end - segment_direction]; + const float2 segment_b = this->curves_2d.points_2d[segment_point_offset + segment_point_end]; + + /* Get curve head and tail points. */ + const int curve_point_offset = this->curves_2d.point_offset[curve_index]; + const int curve_point_head = 0; + const float2 curve_head_p0 = this->curves_2d.points_2d[curve_point_offset]; + const float2 curve_head_p1 = this->curves_2d.points_2d[curve_point_offset + 1]; + const int curve_point_tail = this->curves_2d.point_size[curve_index] - 2; + const float2 curve_tail_p0 = this->curves_2d.points_2d[curve_point_offset + curve_point_tail]; + const float2 curve_tail_p1 = + this->curves_2d.points_2d[curve_point_offset + curve_point_tail + 1]; + + /* When the segment curve and the proximity curve are the same (self-closing curve), take the + * opposite point of the segment end. */ + float2 curve_p0, curve_p1; + int curve_point_index; + bool take_tail; + if (segment_curve_index == curve_index) { + take_tail = (segment_point_end == 0); + } + else { + /* Otherwise, take the curve point closest to the segment end. */ + take_tail = (math::length_squared(segment_b - curve_tail_p1) < + math::length_squared(segment_b - curve_head_p0)); + } + if (take_tail) { + copy_v2_v2(curve_p0, curve_tail_p0); + copy_v2_v2(curve_p1, curve_tail_p1); + curve_point_index = curve_point_tail; + } + else { + copy_v2_v2(curve_p0, curve_head_p0); + copy_v2_v2(curve_p1, curve_head_p1); + curve_point_index = curve_point_head; + } + + /* Check proximity. */ + ProximityCurve proxim; + bool in_proximity; + std::tie(in_proximity, proxim.use_segment_point_end, proxim.curve_point_start_is_nearest) = + this->line_segments_are_in_proximity( + segment_a, segment_b, curve_p0, curve_p1, this->proximity_distance_squared); + + /* Create proximity data. */ + if (!in_proximity) { + proxim.curve_index_2d = -1; + } + else { + proxim.curve_index_2d = curve_index; + proxim.curve_point_start = curve_point_index; + proxim.curve_point_end = curve_point_index + 1; + proxim.segment_point_start = segment_point_end - segment_direction; + proxim.segment_point_end = segment_point_end; + } + + return proxim; + } + + /** + * Get all curves in proximity of an fill edge segment. + */ + Vector get_curves_in_proximity_of_segment(EdgeSegment &segment, + const int segment_direction) + { + /* Inits. */ + Vector proximities; + ThreadMutex mutex = BLI_MUTEX_INITIALIZER; + rctf bbox_segment; + + const int segment_curve_index = segment.curve_index_2d; + const int segment_point_offset = this->curves_2d.point_offset[segment_curve_index]; + + const bool at_end_of_curve = ((segment_direction == -1 && segment.point_range[0] == 0) || + (segment_direction == 1 && + segment.point_range[1] == + this->curves_2d.point_size[segment.curve_index_2d] - 1)); + + EdgeSegment previous_segment; + bool has_previous_segment = false; + if (this->segments.size() > 1) { + previous_segment = this->segments[this->segments.size() - 2]; + has_previous_segment = true; + } + const int prev_segment_curve_index = has_previous_segment ? previous_segment.curve_index_2d : + -1; + + BLI_rctf_init_minmax(&bbox_segment); + BLI_rctf_do_minmax_v(&bbox_segment, + this->curves_2d.points_2d[segment_point_offset + segment.point_range[0]]); + BLI_rctf_do_minmax_v(&bbox_segment, + this->curves_2d.points_2d[segment_point_offset + segment.point_range[1]]); + BLI_rctf_pad(&bbox_segment, this->proximity_distance, this->proximity_distance); + + /* Check all curves for proximity. */ + threading::parallel_for( + this->curves_2d.point_offset.index_range(), 256, [&](const IndexRange range) { + for (const int curve_i : range) { + /* Only check curves with at least two points. */ + if (this->curves_2d.point_size[curve_i] < 2) { + continue; + } + + /* In general, we don't check the current segment curve and the previous segment curve + * for proximity, because it can cause a lot of jumping back and forth. In the + * following example we would constantly hop between curve a and b, via points + * a3-b1-a2-b2-a1-b3-a0-b4. + * Or hop on curve b, via b0-b10-b1-b9-b2-b8-b3-b7. + * + * \ a3 a2 a1 a0 + * curve a x---x-----x----x-----x + * + * curve b x-----x---x------x-----x-------x + * b0 b1 b2 b3 b4 | + * | + * x----x----x------x-----------x + * b10 b9 b8 b7 + * + * So when checking curve b, we exclude curve a and b from the proximity check. + */ + if (curve_i == segment_curve_index || curve_i == prev_segment_curve_index) { + /* But now we would miss simple one- or two-stroke fills, e.g. a sketchy circle drawn + * in one or two strokes. To somehow handle that, we check the head and tails of the + * curves for proximity. E.g. when the head of curve b is in proximity of the tail of + * curve b, it means that curve b is a self-closing fill. Or when the tail of curve b + * is in proximity of the head of curve a, curve a and b form a closed fill. */ + if (at_end_of_curve) { + ProximityCurve in_proximity = this->get_segment_in_proximity_of_curve_head_tail( + segment, segment_direction, curve_i); + if (in_proximity.curve_index_2d != -1) { + BLI_mutex_lock(&mutex); + proximities.append(in_proximity); + BLI_mutex_unlock(&mutex); + } + } + continue; + } + + /* Skip curves with only a fill material. */ + if (!this->curves_2d.has_stroke[curve_i]) { + continue; + } + + /* Skip curve when it is not overlapping the segment bounding box. */ + rctf bbox_proxim; + BLI_rctf_init(&bbox_proxim, + this->curves_2d.curve_bbox[curve_i].xmin, + this->curves_2d.curve_bbox[curve_i].xmax, + this->curves_2d.curve_bbox[curve_i].ymin, + this->curves_2d.curve_bbox[curve_i].ymax); + BLI_rctf_pad(&bbox_proxim, this->proximity_distance, this->proximity_distance); + if (!BLI_rctf_isect(&bbox_segment, &bbox_proxim, nullptr)) { + continue; + } + + /* Skip curves that intersect the segment (no need to find them again). */ + bool skip_curve = false; + for (const SegmentEnd &segment_end : segment.segment_ends) { + if ((segment_end.flag & gfFlag::WithEndExtension) == false && + segment_end.curve_index_2d == curve_i) + { + skip_curve = true; + break; + } + } + if (skip_curve) { + continue; + } + + /* Skip curves that intersect the previous segment (no need to find them again). */ + if (has_previous_segment) { + for (const SegmentEnd &segment_end : previous_segment.segment_ends) { + if ((segment_end.flag & gfFlag::WithEndExtension) == false && + segment_end.curve_index_2d == curve_i) + { + skip_curve = true; + break; + } + } + if (skip_curve) { + continue; + } + } + + /* Skip curves that have exact overlap with curves intersecting the segment. */ + for (const OverlappingSegment &overlap : this->overlapping_segments) { + if (overlap.overlapping_curve_index == curve_i) { + skip_curve = true; + break; + } + } + if (skip_curve) { + continue; + } + + /* Loop over curve points. */ + bool proximity_found = false; + const int curve_point_offset = this->curves_2d.point_offset[curve_i]; + for (const int point_i : + IndexRange(curve_point_offset, this->curves_2d.point_size[curve_i]).drop_back(1)) + { + const float2 curve_p0 = this->curves_2d.points_2d[point_i]; + const float2 curve_p1 = this->curves_2d.points_2d[point_i + 1]; + + /* Skip when bounding box doesn't overlap with segment bounding box. */ + BLI_rctf_init(&bbox_proxim, curve_p0[0], curve_p0[0], curve_p0[1], curve_p0[1]); + BLI_rctf_do_minmax_v(&bbox_proxim, curve_p1); + BLI_rctf_pad(&bbox_proxim, this->proximity_distance, this->proximity_distance); + if (!BLI_rctf_isect(&bbox_proxim, &bbox_segment, nullptr)) { + continue; + } + + /* Loop over all segment point-pairs. */ + const int segment_start = segment_point_offset + ((segment_direction == 1) ? + segment.point_range[0] : + segment.point_range[1]); + const int segment_end = segment_point_offset + ((segment_direction == 1) ? + segment.point_range[1] : + segment.point_range[0]); + + for (int segment_point_i = segment_start; segment_point_i != segment_end; + segment_point_i += segment_direction) + { + const float2 segment_a = this->curves_2d.points_2d[segment_point_i]; + const float2 segment_b = + this->curves_2d.points_2d[segment_point_i + segment_direction]; + + /* Skip when line segments don't overlap. */ + rctf bbox_seg; + BLI_rctf_init(&bbox_seg, segment_a[0], segment_a[0], segment_a[1], segment_a[1]); + BLI_rctf_do_minmax_v(&bbox_seg, segment_b); + BLI_rctf_pad(&bbox_seg, this->proximity_distance, this->proximity_distance); + if (!BLI_rctf_isect(&bbox_proxim, &bbox_seg, nullptr)) { + continue; + } + + /* Check if the line segments are in proximity of each other. */ + bool in_proximity, use_segment_point_end, curve_point_start_nearest; + std::tie(in_proximity, use_segment_point_end, curve_point_start_nearest) = + this->line_segments_are_in_proximity(segment_a, + segment_b, + curve_p0, + curve_p1, + this->proximity_distance_squared); + + /* When the line segments are in proximity, add the data to the proximity list. */ + if (in_proximity) { + ProximityCurve proxim{}; + proxim.curve_index_2d = curve_i; + proxim.curve_point_start = point_i - curve_point_offset; + proxim.curve_point_end = proxim.curve_point_start + 1; + proxim.segment_point_start = segment_point_i - segment_point_offset; + proxim.segment_point_end = proxim.segment_point_start + segment_direction; + proxim.use_segment_point_end = use_segment_point_end; + proxim.curve_point_start_is_nearest = curve_point_start_nearest; + + BLI_mutex_lock(&mutex); + proximities.append(proxim); + BLI_mutex_unlock(&mutex); + + proximity_found = true; + break; + } + } + + /* Stop checking curve points when proximity to the segment is found. */ + if (proximity_found) { + break; + } + } + } + }); + + return proximities; + } + + /** \} */ + + /* ------------------------------------------------------------------------------ */ + /** \name Geometry Fill methods + * \{ */ + + bool position_equals_previous(const float3 &co, float3 &r_co_prev) + { + if (equals_v3v3(co, r_co_prev)) { + return true; + } + copy_v3_v3(r_co_prev, co); + return false; + } + + /** + * Convert fill edge segments to 3D points. + */ + Vector get_closed_fill_edge_as_3d_points() + { + /* Ensure head starting point and tail end point are an exact match. */ + EdgeSegment &tail = this->segments.last(); + for (EdgeSegment &head : this->segments) { + if ((head.flag & gfFlag::IsUnused) == true) { + continue; + } + if ((head.flag & gfFlag::DirectionBackwards) == true) { + if (head.point_range[1] > tail.point_range[1]) { + head.point_range[1] = tail.point_range[1]; + } + } + else { + if (head.point_range[0] < tail.point_range[0]) { + head.point_range[0] = tail.point_range[0]; + } + } + break; + } + + Vector edge_points; + float3 co_prev = {FLT_MAX, FLT_MAX, FLT_MAX}; + int edge_point_index = 0; + + /* Convert all edge segments to 3D coordinates. */ + for (EdgeSegment &segment : this->segments) { + if ((segment.flag & gfFlag::IsUnused) == true) { + continue; + } + + /* Get original 3D curve. */ + const int drawing_index = this->curves_2d.drawing_index_2d[segment.curve_index_2d]; + const int curve_index = segment.curve_index_2d - this->curves_2d.curve_offset[drawing_index]; + const bke::CurvesGeometry &curves = this->curves_2d.drawings[drawing_index]->geometry.wrap(); + const Span positions = curves.positions(); + const OffsetIndices points_by_curve = curves.points_by_curve(); + const IndexRange points_3d = points_by_curve[curve_index]; + const int point_offset = points_3d.first(); + + /* Append curve points of this segment to 3D edge point array. */ + const int direction = ((segment.flag & gfFlag::DirectionBackwards) == true) ? -1 : 1; + int point_start = segment.point_range[0] + point_offset; + int point_end = segment.point_range[1] + point_offset; + if (direction == -1) { + std::swap(point_start, point_end); + } + point_end += direction; + + for (int point_i = point_start; point_i != point_end; point_i += direction) { + if (!this->position_equals_previous(positions[point_i], co_prev)) { + edge_points.append(positions[point_i]); + edge_point_index++; + } + } + + /* Calculate regular intersection point. */ + if ((segment.flag & gfFlag::IsIntersected) == true) { + const SegmentEnd &segment_end = segment.segment_ends[segment.segment_ends_index]; + const float3 isect_point = edge_points.last() + + (positions[point_offset + segment_end.ori_segment_point_end] - + edge_points.last()) * + segment_end.distance; + if (!this->position_equals_previous(isect_point, co_prev)) { + edge_points.append(isect_point); + edge_point_index++; + } + } + + /* Calculate intersection with curve end extension. */ + if ((segment.flag & gfFlag::GapClosureByExtension) == true && edge_point_index > 1) { + /* Get segment end data. */ + const SegmentEnd &segment_end = segment.segment_ends[segment.segment_ends_index]; + const int extension_index = segment.curve_index_2d * 2 + (direction == 1 ? 1 : 0); + + /* Extend end of the curve. */ + const float3 isect_point = edge_points[edge_point_index - 1] + + (edge_points[edge_point_index - 1] - + edge_points[edge_point_index - 2]) * + this->extension_length_ratio[extension_index] * + segment_end.distance; + if (!this->position_equals_previous(isect_point, co_prev)) { + edge_points.append(isect_point); + edge_point_index++; + } + } + } + + /* Remove last point, since it is the same as the first point. */ + edge_points.remove_last(); + + return edge_points; + } + + /** + * Create GP curve geometry from a closed fill edge. + */ + void create_fill_geometry() + { + /* Ensure active frame (autokey is on, we checked on operator invoke). */ + const bke::greasepencil::Layer *active_layer = this->grease_pencil->get_active_layer(); + if (!this->grease_pencil->get_active_layer()->frames().contains(this->frame_number)) { + bke::greasepencil::Layer &active_layer = *this->grease_pencil->get_active_layer(); + + /* For additive drawing, we duplicate the frame that's currently visible and insert it at the + * current frame. */ + bool frame_created = false; + if (this->additive_drawing) { + if (!this->grease_pencil->insert_duplicate_frame( + active_layer, + *active_layer.frame_key_at(this->frame_number), + this->frame_number, + false)) + { + frame_created = true; + } + } + if (!frame_created) { + /* Otherwise we just insert a blank keyframe. */ + if (!this->grease_pencil->insert_blank_frame( + active_layer, this->frame_number, 0, BEZT_KEYTYPE_KEYFRAME)) + { + return; + } + } + } + + /* Get the edge points in 3D space. */ + Vector fill_points = this->get_closed_fill_edge_as_3d_points(); + if (fill_points.size() <= 2) { + return; + } + + /* Create geometry. */ + const int drawing_index = active_layer->drawing_index_at(this->frame_number); + bke::greasepencil::Drawing &drawing = reinterpret_cast( + this->grease_pencil->drawings()[drawing_index]) + ->wrap(); + bke::CurvesGeometry &curves = drawing.strokes_for_write(); + + const int num_old_curves = curves.curves_num(); + const int num_old_points = curves.points_num(); + curves.resize(num_old_points + fill_points.size(), num_old_curves + 1); + + curves.offsets_for_write()[num_old_curves] = num_old_points; + curves.offsets_for_write()[num_old_curves + 1] = num_old_points + fill_points.size(); + + const OffsetIndices points_by_curve = curves.points_by_curve(); + const IndexRange new_points_range = points_by_curve[curves.curves_num() - 1]; + const IndexRange new_curves_range = IndexRange(num_old_curves, 1); + + /* Set position, radius and opacity attribute. */ + bke::MutableAttributeAccessor attributes = curves.attributes_for_write(); + MutableSpan positions = curves.positions_for_write(); + MutableSpan radii = drawing.radii_for_write(); + MutableSpan opacities = drawing.opacities_for_write(); + positions.slice(new_points_range).copy_from(fill_points); + radii.slice(new_points_range).fill(this->brush->size); + opacities.slice(new_points_range).fill(1.0f); + + /* Make curve cyclic. */ + curves.cyclic_for_write().slice(new_curves_range).fill(true); + + /* Set curve_type attribute. */ + curves.fill_curve_types(new_curves_range, CURVE_TYPE_POLY); + + /* Set vertex color for fill and stroke. */ + const bool use_vertex_color = (this->vc.scene->toolsettings->gp_paint->mode == + GPPAINT_FLAG_USE_VERTEXCOLOR); + const bool use_vertex_color_stroke = use_vertex_color && + ELEM(this->brush->gpencil_settings->vertex_mode, + GPPAINT_MODE_STROKE, + GPPAINT_MODE_BOTH); + const bool use_vertex_color_fill = use_vertex_color && + ELEM(this->brush->gpencil_settings->vertex_mode, + GPPAINT_MODE_FILL, + GPPAINT_MODE_BOTH); + const ColorGeometry4f vertex_color_stroke = + use_vertex_color_stroke ? ColorGeometry4f(this->brush->rgb[0], + this->brush->rgb[1], + this->brush->rgb[2], + this->brush->gpencil_settings->vertex_factor) : + ColorGeometry4f(0.0f, 0.0f, 0.0f, 0.0f); + const ColorGeometry4f vertex_color_fill = + use_vertex_color_fill ? ColorGeometry4f(this->brush->rgb[0], + this->brush->rgb[1], + this->brush->rgb[2], + this->brush->gpencil_settings->vertex_factor) : + ColorGeometry4f(0.0f, 0.0f, 0.0f, 0.0f); + + drawing.vertex_colors_for_write().slice(new_points_range).fill(vertex_color_stroke); + bke::SpanAttributeWriter fill_colors = + attributes.lookup_or_add_for_write_span("fill_color", + bke::AttrDomain::Curve); + fill_colors.span.slice(new_curves_range).fill(vertex_color_fill); + fill_colors.finish(); + + /* Set material. */ + Material *material = BKE_grease_pencil_object_material_ensure_from_active_input_brush( + this->vc.bmain, this->vc.obact, this->brush); + const int material_index = BKE_object_material_index_get(this->vc.obact, material); + + bke::SpanAttributeWriter materials = attributes.lookup_or_add_for_write_span( + "material_index", bke::AttrDomain::Curve); + materials.span.slice(new_curves_range).fill(material_index); + materials.finish(); + + /* Explicitly set all other attributes besides those processed above to default values. */ + Set attributes_to_skip{{"position", + "radius", + "opacity", + "curve_type", + "cyclic", + "vertex_color", + "fill_color", + "material_index"}}; + attributes.for_all( + [&](const bke::AttributeIDRef &id, const bke::AttributeMetaData /*meta_data*/) { + if (attributes_to_skip.contains(id.name())) { + return true; + } + bke::GSpanAttributeWriter attribute = attributes.lookup_for_write_span(id); + const CPPType &type = attribute.span.type(); + GMutableSpan new_data = attribute.span.slice( + attribute.domain == bke::AttrDomain::Point ? new_points_range : new_curves_range); + type.fill_assign_n(type.default_value(), new_data.data(), new_data.size()); + attribute.finish(); + return true; + }); + + curves.curve_types_for_write().last() = CURVE_TYPE_POLY; + curves.update_curve_types(); + + /* Set notifiers. */ + drawing.tag_topology_changed(); + DEG_id_tag_update(&this->grease_pencil->id, ID_RECALC_GEOMETRY); + WM_main_add_notifier(NC_GEOM | ND_DATA, &this->grease_pencil->id); + } + + /** + * Check if the mouse coordinates are inside a closed fill edge. + */ + bool mouse_pos_is_inside_polygon(float2 &mouse_pos, Span polygon) + { + /* Rare edge case: polygon without area. */ + if (polygon.size() < 3) { + return false; + } + + /* Algorithm: draw a long horizontal line from the mouse position and count the number of + * intersections with the polygon. An odd number of intersections means the mouse position is + * inside the polygon. */ + const float2 line_p1 = mouse_pos; + const float2 line_p2 = {line_p1[0] + 20000.0f, line_p1[1]}; + int count = 0; + for (const int i : polygon.index_range().drop_back(1)) { + auto isect = math::isect_seg_seg(polygon[i], polygon[i + 1], line_p1, line_p2); + if (ELEM(isect.kind, isect.LINE_LINE_CROSS, isect.LINE_LINE_EXACT)) { + count++; + } + } + + return (count & 1) == 1; + } + + /** + * Convert fill edge segments to 2D points. + */ + Vector get_closed_fill_edge_as_2d_polygon() + { + Vector points; + + /* We cut some corners here, literally, because we don't calculate the exact intersection point + * of intersections. Since the 2D polygon is only used for a mouse-position-inside-polygon + * check, we can afford to be a little sloppy. */ + for (const EdgeSegment &segment : this->segments) { + if ((segment.flag & gfFlag::IsUnused) == true) { + continue; + } + + const int point_offset = this->curves_2d.point_offset[segment.curve_index_2d]; + const int direction = ((segment.flag & gfFlag::DirectionBackwards) == true) ? -1 : 1; + int point_start = segment.point_range[0]; + int point_end = segment.point_range[1]; + if (direction == -1) { + std::swap(point_start, point_end); + } + point_end += direction; + for (int point_i = point_start; point_i != point_end; point_i += direction) { + points.append(this->curves_2d.points_2d[point_offset + point_i]); + } + } + + return points; + } + + /** + * Check if the mouse click position is inside a closed fill edge. + */ + bool mouse_pos_is_inside_closed_edge() + { + Vector closed_edge = this->get_closed_fill_edge_as_2d_polygon(); + + return this->mouse_pos_is_inside_polygon(this->mouse_pos, closed_edge); + } + + /** + * Split an edge segment into its original and overlapping one. + */ + void split_segment_with_overlap(EdgeSegment &segment, const OverlappingSegment &overlap) + { + const bool segment_backwards = ((segment.flag & gfFlag::DirectionBackwards) == true); + + /* Check if we can replace the entire segment by its overlapping companion. */ + if ((!segment_backwards && segment.point_range[0] == overlap.base_point_range[0]) || + (segment_backwards && segment.point_range[1] == overlap.base_point_range[1])) + { + segment.curve_index_2d = overlap.overlapping_curve_index; + segment.point_range = overlap.overlapping_point_range; + segment.flag &= ~gfFlag::DirectionBackwards; + if (overlap.overlapping_backwards) { + segment.flag |= gfFlag::DirectionBackwards; + } + return; + } + + /* Shrink the segment and add another one for the overlapping part. */ + if (segment_backwards) { + segment.point_range[0] = overlap.base_point_range[1]; + } + else { + segment.point_range[1] = overlap.base_point_range[0]; + } + segment.flag &= ~gfFlag::IsIntersected; + + EdgeSegment new_seg{}; + new_seg.curve_index_2d = overlap.overlapping_curve_index; + new_seg.point_range = overlap.overlapping_point_range; + new_seg.flag |= gfFlag::IsPartOfSet; + if (overlap.overlapping_backwards) { + new_seg.flag |= gfFlag::DirectionBackwards; + } + this->segments.append(std::move(new_seg)); + } + + /** + * Remove redundant tail points from a closed fill edge. + */ + void remove_overlapping_edge_tail_points(const EdgeSegment &head, + EdgeSegment &tail, + const int2 &range_head, + int2 &range_tail) + { + /* Get head starting point. */ + const int head_start = ((head.flag & gfFlag::DirectionBackwards) == true) ? range_head[1] : + range_head[0]; + + /* Limit tail to head starting point. */ + if ((tail.flag & gfFlag::DirectionBackwards) == true) { + if (head_start <= range_tail[1]) { + range_tail[0] = head_start; + } + else { + range_tail[0] = range_tail[1]; + } + } + else { + if (head_start >= range_tail[0]) { + range_tail[1] = head_start; + } + else { + range_tail[1] = range_tail[0]; + } + } + + /* Clear tail end flags. */ + tail.flag &= ~gfFlag::GapClosureByExtension; + tail.flag &= ~gfFlag::GapClosureByProximity; + tail.flag &= ~gfFlag::IsIntersected; + } + + /** + * Check if edge segments overlap. This includes a check of alternative segments, + * that share points with the original one. + */ + bool segments_have_overlap(EdgeSegment &head, EdgeSegment &tail) + { + /* Check direct overlap of head and tail.*/ + if (head.curve_index_2d == tail.curve_index_2d) { + /* Check overlap in curve points. */ + if (head.point_range[0] <= tail.point_range[1] && head.point_range[1] >= tail.point_range[0]) + { + this->remove_overlapping_edge_tail_points(head, tail, head.point_range, tail.point_range); + return true; + } + return false; + } + + /* Check alternative segments that have an overlap with the tail segment. */ + for (const OverlappingSegment &overlap : this->overlapping_segments) { + if (head.curve_index_2d == overlap.overlapping_curve_index && + head.point_range[0] <= overlap.overlapping_point_range[1] && + head.point_range[1] >= overlap.overlapping_point_range[0]) + { + this->split_segment_with_overlap(tail, overlap); + EdgeSegment &new_tail = this->segments.last(); + this->remove_overlapping_edge_tail_points( + head, new_tail, head.point_range, new_tail.point_range); + return true; + } + } + + return false; + } + + /** + * Check if a set of edge segments form a closed loop. + */ + bool is_closed_fill_edge() + { + /* Init: by default all segments are used in the edge. */ + for (EdgeSegment &segment : this->segments) { + segment.flag &= ~gfFlag::IsUnused; + } + + /* Loop through the edge segments and see if there is overlap with the last one. */ + EdgeSegment &tail = this->segments.last(); + for (const int segment_i : this->segments.index_range().drop_back(1)) { + if (this->segments_have_overlap(this->segments[segment_i], tail)) { + return true; + } + this->segments[segment_i].flag |= gfFlag::IsUnused; + } + + return false; + } + + /** + * Apply epsilon to an angle around PI and 2 * PI. + */ + float get_rounded_angle(const float angle) + { + float rounded = angle; + + if (rounded > M_PI - ANGLE_EPSILON && rounded <= M_PI) { + rounded = M_PI; + } + + if (rounded >= (2 * M_PI - ANGLE_EPSILON)) { + rounded -= 2 * M_PI; + if (rounded < 0.0f) { + rounded = 0.0f; + } + } + + return rounded; + } + + /** + * Get the next curve point index. Handles cyclic curves. + */ + int get_next_curve_point(const int point_i, + const int direction, + const int point_size, + const bool is_cyclic) + { + int point_next = point_i + direction; + + if (is_cyclic) { + if (point_next < 0) { + point_next = point_size - 1; + } + if (point_next >= point_size) { + point_next = 0; + } + return point_next; + } + + if (point_next >= point_size) { + point_next = -1; + } + + return point_next; + } + + /** + * Add proximity 'end' information to an edge segment: + * - Curve index and curve point index of curves in proximity. + * - Determine the turns (angles) for the proximity curves. + */ + void add_curves_in_proximity_of_segment(EdgeSegment &segment) + { + /* Get curves in proximity of segment. */ + const int segment_direction = ((segment.flag & gfFlag::DirectionBackwards) == true ? -1 : 1); + Vector proximities = this->get_curves_in_proximity_of_segment( + segment, segment_direction); + + /* Process the proximity curves. */ + for (const ProximityCurve &proxim : proximities) { + /* Copy proximity data. */ + SegmentEnd segment_end{}; + segment_end.curve_index_2d = proxim.curve_index_2d; + segment_end.ori_segment_point_start = proxim.segment_point_start; + segment_end.ori_segment_point_end = proxim.segment_point_end; + segment_end.flag = gfFlag::GapClosureByProximity; + + /* Get the segment and proximity curve points. */ + const int segment_point_offset = this->curves_2d.point_offset[segment.curve_index_2d]; + const int curve_i = segment_end.curve_index_2d; + const int curve_point_offset = this->curves_2d.point_offset[curve_i]; + const float2 segment_a = + this->curves_2d.points_2d[segment_point_offset + segment_end.ori_segment_point_start]; + const float2 segment_b = + this->curves_2d.points_2d[segment_point_offset + segment_end.ori_segment_point_end]; + const float2 curve_p1 = + this->curves_2d.points_2d[curve_point_offset + proxim.curve_point_start]; + const float2 curve_p2 = + this->curves_2d.points_2d[curve_point_offset + proxim.curve_point_end]; + + /* Determine the angle between the segment and the proximity curve. */ + segment_end.angle_is_set[0] = true; + segment_end.angle_is_set[1] = true; + const float2 segment_vec = segment_b - segment_a; + bool dir_backwards = false; + float angle0 = 0.0f, angle1 = 0.0f; + + if (proxim.curve_point_start_is_nearest) { + segment_end.point_end = proxim.curve_point_start; + angle0 = this->get_rounded_angle(M_PI - + angle_signed_v2v2(segment_vec, curve_p2 - segment_a)); + + const int curve_point_prev = this->get_next_curve_point( + proxim.curve_point_start, + -1, + this->curves_2d.point_size[curve_i], + this->curves_2d.is_cyclic[curve_i]); + if (curve_point_prev == -1) { + segment_end.angle_is_set[1] = false; + angle1 = 0.0f; + segment_end.point_start = proxim.curve_point_start; + } + else { + const float2 curve_p0 = this->curves_2d.points_2d[curve_point_offset + curve_point_prev]; + angle1 = this->get_rounded_angle(M_PI - + angle_signed_v2v2(segment_vec, curve_p0 - segment_a)); + + if (angle0 < angle1) { + segment_end.point_start = proxim.curve_point_end; + } + else { + dir_backwards = true; + segment_end.point_start = proxim.curve_point_start; + segment_end.point_end = curve_point_prev; + } + } + } + else { + segment_end.point_end = proxim.curve_point_end; + angle0 = this->get_rounded_angle(M_PI - + angle_signed_v2v2(segment_vec, curve_p1 - segment_a)); + + const int curve_point_next = this->get_next_curve_point( + proxim.curve_point_end, + 1, + this->curves_2d.point_size[curve_i], + this->curves_2d.is_cyclic[curve_i]); + if (curve_point_next == -1) { + segment_end.angle_is_set[1] = false; + angle1 = 0.0f; + segment_end.point_start = proxim.curve_point_end; + dir_backwards = true; + } + else { + const float2 curve_p3 = this->curves_2d.points_2d[curve_point_offset + curve_point_next]; + angle1 = this->get_rounded_angle(M_PI - + angle_signed_v2v2(segment_vec, curve_p3 - segment_a)); + + if (angle0 < angle1) { + dir_backwards = true; + segment_end.point_start = proxim.curve_point_start; + } + else { + segment_end.point_start = proxim.curve_point_end; + segment_end.point_end = curve_point_next; + } + } + } + + segment_end.angle[0] = angle0; + segment_end.angle[1] = angle1; + segment_end.angle_min = (segment_end.first_angle_is_smallest() ? angle0 : angle1); + segment_end.angle_max = (segment_end.first_angle_is_largest() ? angle0 : angle1); + if (dir_backwards) { + segment_end.flag |= gfFlag::DirectionBackwards; + } + + /* When using the end point of the segment, shift the point range of the segment + * by one. */ + if (proxim.use_segment_point_end) { + segment_end.ori_segment_point_start = segment_end.ori_segment_point_end; + segment_end.ori_segment_point_end += segment_direction; + + if (segment_end.ori_segment_point_end < segment.point_range[0] || + segment_end.ori_segment_point_end > segment.point_range[1]) + { + segment_end.ori_segment_point_end = -1; + } + } + + /* Append to segment end list. */ + segment.segment_ends.append(segment_end); + } + + /* Sort proximity segment ends on segment start point and angle. */ + std::sort(segment.segment_ends.begin(), + segment.segment_ends.end(), + [segment_direction](const SegmentEnd &a, const SegmentEnd &b) { + if ((a.flag & gfFlag::GapClosureByProximity) != + (b.flag & gfFlag::GapClosureByProximity)) { + return (a.flag & gfFlag::GapClosureByProximity) == false; + } + if (a.ori_segment_point_start == b.ori_segment_point_start) { + if (compare_ff(a.angle_min, b.angle_min, ANGLE_EPSILON)) { + return a.angle_max < b.angle_max; + } + return a.angle_min < b.angle_min; + } + if (segment_direction == 1) { + return a.ori_segment_point_start < b.ori_segment_point_start; + } + return a.ori_segment_point_start > b.ori_segment_point_start; + }); + + /* Set segment flags. */ + segment.flag &= ~gfFlag::IsIntersected; + segment.flag &= ~gfFlag::GapClosureByExtension; + segment.flag |= gfFlag::GapClosureByProximity; + } + + /** + * Add intersection 'end' information to an edge segment: + * - Does the segment end by curve intersection or curve extension (gap closure)? + * - Determine the turns (intersection angles) for the segment ends. + */ + void add_segment_ends_intersection(const Curves2DSpace &curves_2d, + EdgeSegment &segment, + const float2 &segment_point_a, + const float2 &segment_point_b, + const int segment_point_b_index, + const Span intersections, + const bool is_end_extension) + { + const float2 segment_vec = segment_point_b - segment_point_a; + + for (const IntersectingCurve &intersection : intersections) { + /* Skip intersections before the start segment distance. */ + if ((segment.flag & gfFlag::CheckStartDistance) == true && + intersection.distance[0] < this->start_distance) + { + continue; + } + /* Skip end extensions of cyclic curves. */ + if (is_end_extension && curves_2d.is_cyclic[intersection.curve_index_2d]) { + continue; + } + /* Skip end extensions at distance 0. */ + if (is_end_extension && intersection.distance[0] < DISTANCE_EPSILON) { + continue; + } + + /* Copy intersection data. */ + SegmentEnd segment_end{}; + segment_end.curve_index_2d = intersection.curve_index_2d; + segment_end.point_start = intersection.point_start; + segment_end.point_end = intersection.point_end; + segment_end.ori_segment_point_end = segment_point_b_index; + segment_end.distance = intersection.distance[0]; + if (intersection.has_stroke) { + segment_end.flag |= gfFlag::HasStroke; + } + + /* Inverse the distance when walking backwards on a curve and an intersection with an end + * extension is found. This is because the intersection distances of end extension are + * precalculated and always determined in ascending curve point order. */ + if (is_end_extension && segment_point_b_index != -1 && + (segment.flag & gfFlag::DirectionBackwards) == true) + { + segment_end.distance = 1.0f - intersection.distance[0]; + } + + /* Determine incoming and outgoing angle of the intersection. + * The most common case is where the intersecting segment cuts through segment a-b: + * + * b b + * | | + * p1 | p2 p2 | p1 + * x-->>-----------x or x-----------<<--x + * a1 | a0 a0 | a1 + * | | + * | | + * a a + * + * (a0 = angle0, a1 = angle1) + */ + segment_end.angle_is_set[0] = true; + segment_end.angle_is_set[1] = true; + const int curve_i = intersection.curve_index_2d; + const int point_offset = curves_2d.point_offset[curve_i]; + float2 p1 = curves_2d.points_2d[point_offset + intersection.point_start]; + float2 p2 = curves_2d.points_2d[point_offset + intersection.point_end]; + + float angle0 = this->get_rounded_angle(M_PI - angle_signed_v2v2(segment_vec, p2 - p1)); + float angle1 = this->get_rounded_angle(angle0 + M_PI); + bool dir_backwards = (angle0 >= M_PI); + + /* Handle the edge case where the intersecting segment starts at segment a-b. E.g.: + * + * b b + * | | + * p1 | p2 p1 | a0 p2 + * x-->>---x or x-->>---x + * / | a0 |\ + * a1 / | | \ a1 + * / | | \ + * p0 x | | x p0 + * a a + */ + if (!is_end_extension && compare_ff(intersection.distance[1], 0.0f, DISTANCE_EPSILON)) { + dir_backwards = false; + const int point_prev = this->get_next_curve_point(intersection.point_start, + -1, + curves_2d.point_size[curve_i], + curves_2d.is_cyclic[curve_i]); + if (point_prev == -1) { + segment_end.angle_is_set[1] = false; + } + else { + float2 p0 = curves_2d.points_2d[point_offset + point_prev]; + angle1 = get_rounded_angle(M_PI - angle_signed_v2v2(segment_vec, p0 - p1)); + dir_backwards = (angle1 < angle0); + } + } + + /* Handle the edge case where the intersecting segment ends at segment a-b. E.g.: + * + * b b + * | | + * p1 | p2 p1 a0 | p2 + * x-->>---x or x-->>---x + * a0 |\ /| + * | \ a1 a1 / | + * | \ / | + * | x p3 p3 x | + * a a + */ + if (!is_end_extension && compare_ff(intersection.distance[1], 1.0f, DISTANCE_EPSILON)) { + dir_backwards = true; + angle0 = angle1; + const int point_next = this->get_next_curve_point(intersection.point_end, + 1, + curves_2d.point_size[curve_i], + curves_2d.is_cyclic[curve_i]); + if (point_next == -1) { + segment_end.angle_is_set[1] = false; + } + else { + float2 p3 = curves_2d.points_2d[point_offset + point_next]; + angle1 = this->get_rounded_angle(M_PI - angle_signed_v2v2(segment_vec, p3 - p2)); + dir_backwards = (angle1 > angle0); + } + } + + /* Set the curve direction on the (first) right turn. */ + if (dir_backwards) { + segment_end.flag |= gfFlag::DirectionBackwards; + } + + /* Handle intersection by end extension. */ + if (is_end_extension) { + segment_end.curve_index_2d = int(intersection.curve_index_2d / 2); + segment_end.flag = gfFlag::WithEndExtension; + angle0 = angle1; + + /* Set curve direction: backwards for extension at end of curve. */ + if ((intersection.curve_index_2d & 1) == 1) { + segment_end.flag |= gfFlag::DirectionBackwards; + segment_end.point_start = this->curves_2d.point_size[segment_end.curve_index_2d] - 1; + } + segment_end.point_end = intersection.point_start; + + /* Set stroke material flag. */ + if (this->curves_2d.has_stroke[segment_end.curve_index_2d]) { + segment_end.flag |= gfFlag::HasStroke; + } + } + + /* Clear angle when parallel to segment a-b. */ + if (compare_ff(angle0, 0.0f, ANGLE_EPSILON)) { + segment_end.angle_is_set[0] = false; + } + if (compare_ff(angle1, 0.0f, ANGLE_EPSILON) || is_end_extension) { + segment_end.angle_is_set[1] = false; + } + if (!(segment_end.angle_is_set[0] || segment_end.angle_is_set[1])) { + continue; + } + + /* Store angles and determine minimum/maximum. */ + segment_end.angle[0] = angle0; + segment_end.angle[1] = angle1; + segment_end.angle_min = (segment_end.first_angle_is_smallest() ? angle0 : angle1); + segment_end.angle_max = (segment_end.first_angle_is_largest() ? angle0 : angle1); + + /* Append to segment end list. */ + segment.segment_ends.append(segment_end); + } + } + + /** + * Expand an edge segment by walking along the curve. An edge segment ends when an intersection + * is found or when the end of the curve is reached. At the end of a curve, gap closure + * inspection is performed. + */ + bool walk_along_curve(EdgeSegment &segment, const int started_at = -1) + { + /* Skip when segment is already inspected. */ + if ((segment.flag & gfFlag::IsInspected) == true) { + return false; + } + segment.flag |= gfFlag::IsInspected; + + /* Get curve point start, range etc. */ + const int curve_i = segment.curve_index_2d; + const bool is_cyclic = this->curves_2d.is_cyclic[curve_i]; + const int direction = ((segment.flag & gfFlag::DirectionBackwards) == true) ? -1 : 1; + const int point_size = this->curves_2d.point_size[curve_i]; + const int point_offset = this->curves_2d.point_offset[curve_i]; + int point_i = segment.point_range[0]; + const int point_started = (started_at == -1 ? point_i : started_at); + + float2 segment_point_a = {FLT_MAX, FLT_MAX}; + const int point_prev = this->get_next_curve_point(point_i, -direction, point_size, is_cyclic); + if (point_prev != -1) { + segment_point_a = this->curves_2d.points_2d[point_offset + point_prev]; + } + + this->overlapping_segments.clear(); + + bool check_gaps = false; + + /* Walk along the points of a curve until: + * - An intersection with another curve is found. + * - A curve in proximity is found. + * - An intersection with a curve end extension is found. + * Abort when the end of the curve is reached. */ + do { + /* Check curve bounds. */ + if (point_i < 0 || point_i >= point_size) { + point_i = std::clamp(point_i, 0, point_size - 1); + if (!is_cyclic) { + check_gaps = true; + break; + } + + /* Add new segment for second point range of a cyclic curve. */ + segment.set_point_range(point_i, direction); + segment.flag &= ~gfFlag::CheckStartDistance; + + point_i = (direction == -1) ? (point_size - 1) : 0; + + EdgeSegment new_segment{}; + new_segment.curve_index_2d = segment.curve_index_2d; + new_segment.init_point_range(point_i); + new_segment.flag = segment.flag; + new_segment.flag |= gfFlag::IsPartOfSet; + if (point_i != point_started) { + new_segment.flag &= ~gfFlag::IsInspected; + } + this->segments.append(new_segment); + + if (point_i == point_started) { + return true; + } + + /* Walk along second part of cyclic curve. */ + this->walk_along_curve(this->segments.last(), point_started); + return true; + } + + /* Get the next point on curve, so that we have a point pair a-b. */ + const int point_next = this->get_next_curve_point(point_i, direction, point_size, is_cyclic); + if (point_next == -1) { + check_gaps = true; + break; + } + + /* Get the coordinates of points a-b, and their neighbours. */ + const float2 point_a_prev = segment_point_a; + segment_point_a = this->curves_2d.points_2d[point_offset + point_i]; + const float2 segment_point_b = this->curves_2d.points_2d[point_offset + point_next]; + float2 point_b_next = {FLT_MAX, FLT_MAX}; + const int point_next2 = this->get_next_curve_point( + point_next, direction, point_size, is_cyclic); + if (point_next2 != -1) { + point_b_next = this->curves_2d.points_2d[point_offset + point_next2]; + } + + /* Get intersections with other curves. */ + Vector intersections = this->get_intersections_of_segment_with_curves( + segment_point_a, + segment_point_b, + curve_i, + this->curves_2d, + point_a_prev, + point_b_next, + true, + point_i, + point_next, + direction, + true); + this->add_segment_ends_intersection(this->curves_2d, + segment, + segment_point_a, + segment_point_b, + point_next, + intersections, + false); + + /* Get intersections with curve end extensions. */ + if (this->use_gap_close_extend) { + intersections.clear(); + + /* Find extension intersection by serial lookup (they have already been calculated in the + * interactive step of the modal operator). + * Note: we deliberately create a copy of the intersection, because we change the values. + */ + for (IntersectingCurve intersection : this->extension_intersections) { + if (intersection.with_end_extension) { + continue; + } + + /* Look for intersections with the current curve segment. */ + if (intersection.curve_index_2d == curve_i && + ((intersection.point_start == point_i && intersection.point_end == point_next) || + (intersection.point_start == point_next && intersection.point_end == point_i))) + { + intersection.curve_index_2d = intersection.extension_index; + intersection.point_start = 0; + intersection.point_end = 1; + std::swap(intersection.distance[0], intersection.distance[1]); + intersections.append(intersection); + } + } + + this->add_segment_ends_intersection(this->extensions_2d, + segment, + segment_point_a, + segment_point_b, + point_next, + intersections, + true); + } + + /* TODO: Get curves in proximity. */ + if (this->use_gap_close_proximity) { + } + + /* When one or more intersections are found, we can stop walking along the curve. */ + if (!segment.segment_ends.is_empty()) { + /* Sort intersections on distance and angle. This is needed to find the narrowest edge. + * At equal distance, give priority to a curve with a stroke material. */ + std::sort(segment.segment_ends.begin(), + segment.segment_ends.end(), + [](const SegmentEnd &a, const SegmentEnd &b) { + if (compare_ff(a.distance, b.distance, DISTANCE_SORT_EPSILON)) { + if ((a.flag & gfFlag::HasStroke) != (b.flag & gfFlag::HasStroke)) { + return (a.flag & gfFlag::HasStroke) == true; + } + if (compare_ff(a.angle_min, b.angle_min, ANGLE_EPSILON)) { + return a.angle_max < b.angle_max; + } + return a.angle_min < b.angle_min; + } + return a.distance < b.distance; + }); + + /* Set intersection flag. */ + segment.flag |= gfFlag::IsIntersected; + + break; + } + + /* Walk to next point on curve. */ + segment.flag &= ~gfFlag::CheckStartDistance; + point_i += direction; + + /* Check closed cycle. */ + if (point_i == point_started) { + break; + } + } while (true); + + /* Set walked point range. */ + segment.flag &= ~gfFlag::CheckStartDistance; + segment.set_point_range(point_i, direction); + + /* When the edge segment is ended with an intersection, we are done now. + * If not, we have reached the end of the curve and we will check for gap closures. */ + if (!check_gaps) { + return true; + } + + /* Check for gap closure by extending the end of the curve. */ + if (this->use_gap_close_extend) { + /* Get the coordinates of start/end extension. */ + const int extension_index = curve_i * 2 + (direction == 1 ? 1 : 0); + const int point_offset = this->extensions_2d.point_offset[extension_index]; + const float2 segment_point_a = this->extensions_2d.points_2d[point_offset]; + const float2 segment_point_b = this->extensions_2d.points_2d[point_offset + 1]; + + /* Abort when the end extension doesn't intersect with anything. */ + if (!this->extension_has_intersection[extension_index]) { + return true; + } + + /* Find the intersections of the end extension by serial lookup. The intersection data is + * already calculated during the interactive step of the modal operator. */ + Vector intersections_curves; + Vector intersections_extensions; + for (const IntersectingCurve &intersection : this->extension_intersections) { + if (intersection.extension_index != extension_index) { + continue; + } + + if (intersection.with_end_extension) { + intersections_extensions.append(intersection); + } + else { + intersections_curves.append(intersection); + } + } + + /* Add intersections with other curve. */ + this->add_segment_ends_intersection(this->curves_2d, + segment, + segment_point_a, + segment_point_b, + -1, + intersections_curves, + false); + + /* Add intersections with other curve end extension. */ + this->add_segment_ends_intersection(this->extensions_2d, + segment, + segment_point_a, + segment_point_b, + -1, + intersections_extensions, + true); + + if (!segment.segment_ends.is_empty()) { + /* Sort intersections on distance and angle. This is important to find the narrowest edge. + */ + std::sort(segment.segment_ends.begin(), + segment.segment_ends.end(), + [](const SegmentEnd &a, const SegmentEnd &b) { + if (compare_ff(a.distance, b.distance, DISTANCE_SORT_EPSILON)) { + if ((a.flag & gfFlag::HasStroke) != (b.flag & gfFlag::HasStroke)) { + return (a.flag & gfFlag::HasStroke) == true; + } + if (compare_ff(a.angle_min, b.angle_min, ANGLE_EPSILON)) { + return a.angle_max < b.angle_max; + } + return a.angle_min < b.angle_min; + } + return a.distance < b.distance; + }); + + /* Set gap closure flag. */ + segment.flag |= gfFlag::GapClosureByExtension; + } + } + + /* Check for gap closure with radii at the ends of curves. */ + if (this->use_gap_close_radius) { + /* Get coordinates of curve start/end point. */ + const int curve_point_end = point_offset + point_i; + const float2 curve_point = this->curves_2d.points_2d[curve_point_end]; + const bool at_start_of_curve = (point_i == 0); + + /* Get the segment coordinates at the start/end of the curve. */ + const int curve_point_end_prev = curve_point_end + + math::min(point_size - 1, 1) * (at_start_of_curve ? 1 : -1); + const float2 curve_point_prev = this->curves_2d.points_2d[curve_point_end_prev]; + const float2 curve_end_vec = curve_point - curve_point_prev; + const float max_dist = 2 * this->gap_distance; + + /* Find nearest end points. + * Note: the number of points is a bit arbitrary, but the defined number will suffice for + * normal cases. */ + KDTreeNearest_2d nearest[RADIUS_NEAREST_NUM]; + const int nearest_num = BLI_kdtree_2d_find_nearest_n( + this->curve_ends, curve_point, nearest, RADIUS_NEAREST_NUM); + + for (int i = 0; i < nearest_num; i++) { + /* Skip nearest points outside the user-defined closing radius. */ + if (nearest[i].dist > max_dist) { + continue; + } + + /* There are (number of curves * 2) points in the KD tree: the first and last point of each + * curve. So an even KD tree index means a 'first' point, an odd index means a 'last' + * point. */ + const int nearest_curve = int(nearest[i].index / 2); + const bool nearest_at_start_of_curve = (nearest[i].index & 1) == 0; + const int nearest_point_offset = this->curves_2d.point_offset[nearest_curve]; + const int nearest_point_i = (nearest_at_start_of_curve ? + 0 : + this->curves_2d.point_size[nearest_curve] - 1); + + /* Skip self (the curve end we are gap-closing). */ + if (nearest_curve == curve_i && nearest_at_start_of_curve == at_start_of_curve) { + continue; + } + /* Skip cyclic curves. */ + if (this->curves_2d.is_cyclic[nearest_curve]) { + continue; + } + + /* Set segment end values. */ + SegmentEnd segment_end{}; + segment_end.flag = (nearest_at_start_of_curve ? gfFlag::None : gfFlag::DirectionBackwards); + segment_end.curve_index_2d = nearest_curve; + segment_end.point_start = nearest_point_i; + + /* Determine the angle between the curve start/end and the nearest point. For finding the + * narrowest fill edge, we explore the smallest angle first (= most right turn). */ + const float2 nearest_vec = + this->curves_2d.points_2d[nearest_point_offset + nearest_point_i] - curve_point; + segment_end.angle[0] = M_PI - angle_signed_v2v2(curve_end_vec, nearest_vec); + segment_end.angle_is_set[0] = true; + segment_end.angle_is_set[1] = false; + + /* Append to segment end list. */ + segment.segment_ends.append(segment_end); + } + + if (!segment.segment_ends.is_empty()) { + /* Sort nearest curves on angle. */ + std::sort( + segment.segment_ends.begin(), + segment.segment_ends.end(), + [](const SegmentEnd &a, const SegmentEnd &b) { return a.angle[0] < b.angle[0]; }); + } + } + + return true; + } + + /** + * Get the next turn on a curve intersection or gap closure. + */ + void take_next_turn_on_intersection(SegmentEnd &segment_end, const EdgeSegment &ori_segment) + { + gfSegmentTurn turn[3] = {gfSegmentTurn::None}; + int first_angle_index, second_angle_index, straight_ahead_index; + const bool at_end_extension = ((ori_segment.flag & gfFlag::IsIntersected) == false) && + ((segment_end.flag & gfFlag::GapClosureByProximity) == false); + + /* The most common case is a three-way intersection: right turn, straight ahead and left turn. + * But we have to handle the edge cases: the intersecting curve on one side only, skipping + * redundant turns etc. */ + if (segment_end.angle_min >= M_PI) { + /* Intersecting curve entirely on the left side. */ + straight_ahead_index = 0; + first_angle_index = 1; + second_angle_index = 2; + } + else if (segment_end.angle_max < M_PI) { + /* Intersecting curve entirely on the right side. */ + first_angle_index = 0; + second_angle_index = 1; + straight_ahead_index = 2; + } + else { + /* Intersecting curve on right and left side. */ + first_angle_index = 0; + straight_ahead_index = 1; + second_angle_index = 2; + } + + /* Set the turns. */ + turn[first_angle_index] = gfSegmentTurn::FirstAngle; + turn[straight_ahead_index] = gfSegmentTurn::StraightAhead; + turn[second_angle_index] = gfSegmentTurn::SecondAngle; + + /* Clear the turn when the angle is undefined. */ + gfSegmentTurn current_turn = turn[segment_end.turn]; + if (current_turn == gfSegmentTurn::FirstAngle && !segment_end.angle_is_set[0]) { + current_turn = gfSegmentTurn::None; + } + if (current_turn == gfSegmentTurn::SecondAngle && !segment_end.angle_is_set[1]) { + current_turn = gfSegmentTurn::None; + } + + /* Clear the straight ahead turn when at an end extension. */ + if (at_end_extension && current_turn == gfSegmentTurn::StraightAhead) { + current_turn = gfSegmentTurn::None; + } + + /* Clear the straight ahead turn when there is no segment point left to follow.*/ + if (current_turn == gfSegmentTurn::StraightAhead && segment_end.ori_segment_point_end == -1) { + current_turn = gfSegmentTurn::None; + } + + /* Abort when turn is undefined. */ + if (current_turn == gfSegmentTurn::None) { + return; + } + + /* Take the turn. */ + int curve_index_2d, point_start; + gfFlag flag = gfFlag::None; + + switch (current_turn) { + /* Right turn (cq. smallest angle). */ + case gfSegmentTurn::FirstAngle: { + curve_index_2d = segment_end.curve_index_2d; + point_start = segment_end.point_end; + if ((segment_end.flag & gfFlag::DirectionBackwards) == true) { + point_start = segment_end.point_start; + flag = gfFlag::DirectionBackwards; + } + break; + } + /* Straight ahead, on same curve as we were. */ + case gfSegmentTurn::StraightAhead: { + curve_index_2d = ori_segment.curve_index_2d; + point_start = segment_end.ori_segment_point_end; + if ((ori_segment.flag & gfFlag::DirectionBackwards) == true) { + flag = gfFlag::DirectionBackwards; + } + break; + } + /* Left turn (cq. largest angle). */ + case gfSegmentTurn::SecondAngle: { + curve_index_2d = segment_end.curve_index_2d; + point_start = segment_end.point_start; + flag = gfFlag::DirectionBackwards; + if ((segment_end.flag & gfFlag::DirectionBackwards) == true) { + point_start = segment_end.point_end; + flag = gfFlag::None; + } + break; + } + default: + break; + } + + /* Add new segment to the fill edge. */ + EdgeSegment segment{}; + segment.curve_index_2d = curve_index_2d; + segment.init_point_range(point_start); + segment.flag = flag; + + this->segments.append(segment); + } + + /** + * Get the next curve found by gap closure. + */ + void take_next_gap_closure_curve(SegmentEnd &segment_end) + { + /* Add new segment to the fill edge. */ + EdgeSegment segment{}; + segment.curve_index_2d = segment_end.curve_index_2d; + segment.init_point_range(segment_end.point_start); + if ((segment_end.flag & gfFlag::DirectionBackwards) == true) { + segment.flag = gfFlag::DirectionBackwards; + } + + this->segments.append(segment); + } + + /** + * Remove the last edge segment. Handles segment sets. + */ + void remove_last_segment() + { + /* Remove segment. */ + EdgeSegment &segment = this->segments.last(); + const bool is_part_of_set = ((segment.flag & gfFlag::IsPartOfSet) == true); + this->segments.remove_last(); + + /* Repeat when segment is part of a set. */ + if (is_part_of_set) { + this->remove_last_segment(); + } + } + +#ifdef GP_FILL_DEBUG_MODE + /** + * Debug function: print edge segment data to the console. + */ + void debug_print_segment_data(EdgeSegment &segment, const bool proximity_only) + { + if (!proximity_only) { + printf("Segments: %lld, walked along: curve %d (%d) %d-%d, flag %d,%s end segments %lld \n", + this->segments.size(), + segment.curve_index_2d, + this->curves_2d.point_size[segment.curve_index_2d], + segment.point_range[0], + segment.point_range[1], + segment.flag, + (segment.flag & gfFlag::DirectionBackwards) == true ? " backwards," : "", + segment.segment_ends.size()); + } + + for (const SegmentEnd &seg_end : segment.segment_ends) { + if (!proximity_only || (seg_end.flag & gfFlag::GapClosureByProximity) == true) { + printf(" - segment end: curve %d %d-%d, distance %.3f, angles %.2f %.2f%s%s%s", + seg_end.curve_index_2d, + seg_end.point_start, + seg_end.point_end, + seg_end.distance, + seg_end.angle_is_set[0] ? seg_end.angle[0] : 9.99f, + seg_end.angle_is_set[1] ? seg_end.angle[1] : 9.99f, + (seg_end.flag & gfFlag::DirectionBackwards) == true ? ", backwards" : "", + (seg_end.flag & gfFlag::WithEndExtension) == true ? ", end extension" : "", + (seg_end.flag & gfFlag::GapClosureByProximity) == true ? ", in proximity" : ""); + if ((seg_end.flag & gfFlag::WithEndExtension) == false) { + const int offset = this->curves_2d.point_offset[seg_end.curve_index_2d]; + printf(", %.1f %.1f - %.1f %.1f", + this->curves_2d.points_2d[offset + seg_end.point_start][0], + this->curves_2d.points_2d[offset + seg_end.point_start][1], + this->curves_2d.points_2d[offset + seg_end.point_end][0], + this->curves_2d.points_2d[offset + seg_end.point_end][1]); + } + printf("\n"); + } + } + } +#endif + + /** + * Find a closed fill edge, by creating a chain of edge segments (parts of curves). + * The edge is build in clockwise direction. The narrowest edge is created by taking + * the most right turn first at curve intersections. + */ + bool find_closed_fill_edge() + { +#ifdef GP_FILL_DEBUG_MODE + int iteration = 0; +#endif + + while (!this->segments.is_empty()) { + +#ifdef GP_FILL_DEBUG_MODE + /* Limit number of iterations in debug mode. */ + if (iteration++ > 200) { + printf("Aborted, more than 200 iterations...\n"); + return false; + } +#else + /* Emergency break: when the operator is taking too much time, jump to the conclusion that no + * closed fill edge can be found. */ + auto t2 = std::chrono::high_resolution_clock::now(); + auto delta_t = std::chrono::duration_cast( + t2 - this->operator_time_start); + if (delta_t.count() > MAX_EXECUTION_TIME) { + return false; + } +#endif + + /* Explore the last edge segment in the array. */ + EdgeSegment &segment = this->segments.last(); + + /* Walk along the curve until an intersection or the end of the curve is reached. */ + bool changed = this->walk_along_curve(segment); + segment = this->segments.last(); + +#ifdef GP_FILL_DEBUG_MODE + if (changed) { + this->debug_print_segment_data(segment, false); + } +#endif + + /* Check if the edge is closed. */ + if (changed) { + if (this->is_closed_fill_edge()) { + /* Check if the mouse click position is inside the edge loop. */ + if (this->mouse_pos_is_inside_closed_edge()) { + return true; + } + + /* If the mouse position is outside the edge, remove the closing segment and continue + * searching. */ + this->remove_last_segment(); + continue; + } + } + + /* Inpsect the segment ends. */ + if (segment.segment_ends_index < segment.segment_ends.size()) { + SegmentEnd &segment_end = segment.segment_ends[segment.segment_ends_index]; + + /* When jumping to a curve in proximity, adjust the point range of the segment according to + * the location of the curve. */ + if ((segment_end.flag & gfFlag::GapClosureByProximity) == true) { + if ((segment.flag & gfFlag::DirectionBackwards) == true) { + segment.point_range[0] = std::min(segment_end.ori_segment_point_start, + segment_end.ori_segment_point_end); + } + else { + segment.point_range[1] = std::max(segment_end.ori_segment_point_start, + segment_end.ori_segment_point_end); + } + } + + /* Handle segment that ends at an intersection or gap closure with several turns. */ + if ((segment.flag & gfFlag::IsIntersected) == true || + (segment.flag & gfFlag::GapClosureByProximity) == true || + ((segment.flag & gfFlag::GapClosureByExtension) == true && + (segment_end.flag & gfFlag::WithEndExtension) == false)) + { + /* When all intersection turns are explored, continue with the + * next intersection (if any). */ + if (segment_end.turn >= 3) { + segment.segment_ends_index++; + continue; + } + + /* Take the next turn on the intersection: right, straight ahead, left. */ + this->take_next_turn_on_intersection(segment_end, segment); + + segment_end.turn++; + } + else { + /* Gap closure with radius or with another curve end extension: jump onto the next + * gap-closed curve. */ + if (segment_end.turn >= 1) { + segment.segment_ends_index++; + continue; + } + + this->take_next_gap_closure_curve(segment_end); + segment_end.turn++; + } + } + else if (this->use_gap_close_proximity && + (segment.flag & gfFlag::ProximityInspected) == false && + segment.point_range[0] != segment.point_range[1]) + { + /* Gap closure by proximity: check for curves in proximity of this segment. */ + segment.flag |= gfFlag::ProximityInspected; + this->add_curves_in_proximity_of_segment(segment); + +#ifdef GP_FILL_DEBUG_MODE + this->debug_print_segment_data(segment, true); +#endif + } + else { + /* We reached a dead end, delete the segment and continue with the previous. */ + this->remove_last_segment(); + } + } + + return false; + } + + /** + * Execute the fill operation, at the second mouse click. (The first mouse click is for the + * interactive gap closure phase.) + */ + bool execute_fill() override + { + /* Find a first (arbitrary) edge point of the fill area by casting a ray from the mouse click + * position in one of four directions: up, right, down, left. When this ray crosses a curve, + * we use the intersection point as the starting point of the fill edge. + * We check in four directions, because the user can click near a gap in the fill edge. */ + this->operator_time_start = std::chrono::high_resolution_clock::now(); + this->mutex = BLI_MUTEX_INITIALIZER; + const float ray_translation = 20000.0f; + gfRayDirection ray_direction; + float2 ray_vec; + bool found = false; + + for (gfRayDirection ray_dir : ray_directions) { + ray_direction = ray_dir; + ray_vec = this->mouse_pos; + + switch (ray_dir) { + case gfRayDirection::Up: + ray_vec[1] += ray_translation; + break; + case gfRayDirection::Right: + ray_vec[0] += ray_translation; + break; + case gfRayDirection::Down: + ray_vec[1] -= ray_translation; + break; + case gfRayDirection::Left: + ray_vec[0] -= ray_translation; + break; + } + + this->starting_segments = get_intersections_of_segment_with_curves( + this->mouse_pos, ray_vec, -1, this->curves_2d); + + if (this->starting_segments.size() == 0) { + continue; + } + + /* Sort starting segments on distance (closest first). At equal distance, give priority to a + * curve with stroke material. */ + std::sort(this->starting_segments.begin(), + this->starting_segments.end(), + [](const IntersectingCurve &a, const IntersectingCurve &b) { + if (compare_ff(a.distance[0], b.distance[0], DISTANCE_SORT_EPSILON)) { + if (a.has_stroke != b.has_stroke) { + return a.has_stroke; + } + } + return a.distance[0] < b.distance[0]; + }); + + /* From the first edge point we try to follow the fill edge clockwise, until we find + * a closed loop. */ + IntersectingCurve &start_segment = this->starting_segments.first(); + + /* Start with an empty edge segment list. */ + this->segments.clear(); + + /* Add first edge segment. */ + EdgeSegment segment{}; + segment.curve_index_2d = start_segment.curve_index_2d; + segment.init_point_range(start_segment.point_start); + + /* Set the minimum distance for checking intersections on this first segment. */ + segment.flag = gfFlag::CheckStartDistance; + this->start_distance = start_segment.distance[1]; + + /* We want to follow the fill edge clockwise. Determine in which direction we have to follow + * the curve for that. */ + const int point_offset = this->curves_2d.point_offset[segment.curve_index_2d]; + const float2 segment_a = this->curves_2d.points_2d[point_offset + start_segment.point_start]; + const float2 segment_b = this->curves_2d.points_2d[point_offset + start_segment.point_end]; + const float delta_x = segment_b[0] - segment_a[0]; + const float delta_y = segment_b[1] - segment_a[1]; + switch (ray_direction) { + case gfRayDirection::Up: + if (delta_x < 0) { + segment.flag |= gfFlag::DirectionBackwards; + } + break; + case gfRayDirection::Right: + if (delta_y > 0) { + segment.flag |= gfFlag::DirectionBackwards; + } + break; + case gfRayDirection::Down: + if (delta_x > 0) { + segment.flag |= gfFlag::DirectionBackwards; + } + break; + case gfRayDirection::Left: + if (delta_y < 0) { + segment.flag |= gfFlag::DirectionBackwards; + } + break; + default: + break; + } + if ((segment.flag & gfFlag::DirectionBackwards) == true) { + segment.point_range[0] = start_segment.point_end; + this->start_distance = 1.0f - this->start_distance; + } + + this->segments.append(segment); + + /* See if we can expand this first edge segment to a closed edge. */ + found = this->find_closed_fill_edge(); + if (found) { + break; + } + } + + if (found) { + /* Create the 3D fill curve geometry. */ + this->create_fill_geometry(); + } + + return found; + } + + /** \} */ +}; + +void geometry_fill_exit(const wmOperator &op) +{ + GeometryFillOperation *fill_op = static_cast(op.customdata); + fill_op->fill_exit(); + MEM_delete(fill_op); +} + +bool geometry_fill_exec(const wmOperator &op) +{ + GeometryFillOperation *fill_op = static_cast(op.customdata); + return fill_op->fill_exec(); +} + +int geometry_fill_modal(const wmOperator &op, const wmEvent &event) +{ + GeometryFillOperation *fill_op = static_cast(op.customdata); + return fill_op->fill_modal(op, event); +} + +bool geometry_fill_invoke(bContext *C, wmOperator *op) +{ + GeometryFillOperation *fill_op = MEM_new(__func__); + op->customdata = fill_op; + return fill_op->fill_init(C); +} + +} // namespace blender::ed::greasepencil::fill diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_fill_geometry.hh b/source/blender/editors/grease_pencil/intern/grease_pencil_fill_geometry.hh new file mode 100644 index 00000000000..21a49955f40 --- /dev/null +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_fill_geometry.hh @@ -0,0 +1,201 @@ +/* SPDX-FileCopyrightText: 2023 Blender Foundation + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +/** \file + * \ingroup edgreasepencil + */ + +#pragma once + +#include + +namespace blender::ed::greasepencil::fill { + +/* Margin for angles to be considered equal. */ +static constexpr float ANGLE_EPSILON = 0.005f; +/* Maximum execution time of geometry fill operator (in milliseconds). */ +static constexpr unsigned int MAX_EXECUTION_TIME = 500; + +/* Fill segment and segment end flags. */ +enum class gfFlag : uint16_t { + None = 0, + /* Segment is inspected on intersections and gap closures. */ + IsInspected = 1 << 0, + /* When backwards, point range of segment is from high to low. */ + DirectionBackwards = 1 << 1, + /* Segment is intersected by other curve (or curve end extension.) */ + IsIntersected = 1 << 2, + /* Segment is part of a set (two segments of cyclic curve, or overlapping tail segments). */ + IsPartOfSet = 1 << 3, + /* Segment ends with a gap closure extension to another curve. */ + GapClosureByExtension = 1 << 4, + /* Segment end with a gap closure by proximity to another curve. */ + GapClosureByProximity = 1 << 5, + /* Segment end connects with end extension of other curve. */ + WithEndExtension = 1 << 6, + /* For the first segment only: ignore intersections before the start distance. */ + CheckStartDistance = 1 << 7, + /* Segment curve has stroke material. */ + HasStroke = 1 << 8, + /* Segment is inspected on proximity of curves. */ + ProximityInspected = 1 << 9, + /* Segment is part of the unused head of a closed fill edge. */ + IsUnused = 1 << 10, +}; +ENUM_OPERATORS(gfFlag, gfFlag::IsUnused); +static inline constexpr bool operator==(gfFlag a, bool b) +{ + return (uint64_t(a) != 0) == b; +} + +/* Directions of casted ray to find first edge point of fill. */ +enum class gfRayDirection : uint8_t { + Up = 0, + Right = 1, + Down = 2, + Left = 3, +}; + +constexpr std::initializer_list ray_directions = { + gfRayDirection::Up, gfRayDirection::Right, gfRayDirection::Down, gfRayDirection::Left}; + +/* Possible turns at a curve intersection. */ +enum class gfSegmentTurn : uint8_t { + None = 0, + FirstAngle = 1, + StraightAhead = 2, + SecondAngle = 3, +}; + +/* Proximity curve. Created when searching for curves-in-proximity of fill edge segments. */ +struct ProximityCurve { + /* Curve index in #Curves2DSpace struct. */ + int curve_index_2d; + /* Start point of the curve in proximity (relative index: 0..). */ + int curve_point_start; + /* End point of the curve in proximity (relative index: 0..). */ + int curve_point_end; + /* Start point in the segment. */ + int segment_point_start; + /* End point in the segment. */ + int segment_point_end; + /* Flag for connecting the segment end point to the proximity start point in the fill edge.*/ + bool use_segment_point_end; + /* Flag when the start point of the proximity curve is nearer to the segment than the + * end point. */ + bool curve_point_start_is_nearest; +}; + +/* Edge segment ending: intersection data when a curve or a curve end extension intersects + * the edge segment. */ +struct SegmentEnd { + /* Curve index (in #Curves2DSpace struct) of the intersecting/gap closing curve. */ + int curve_index_2d; + /* Curve point index of the intersection start point. */ + int point_start; + /* Curve point index of the intersection end point. */ + int point_end; + /* Start point index of the original segment that is intersected. */ + int ori_segment_point_start; + /* End point index of the original segment that is intersected. */ + int ori_segment_point_end; + /* Normalized distance of intersection. */ + float distance; + /* Incoming and outgoing angle of the intersection. */ + float2 angle; + /* Minimum angle. */ + float angle_min; + /* Maximum angle. */ + float angle_max; + /* Flag if the incoming and outgoing angle is set. */ + bool angle_is_set[2]; + /* Segment flags. */ + gfFlag flag = gfFlag::None; + /* Turn to take at intersections. */ + short turn; + + bool first_angle_is_smallest() const + { + if (!this->angle_is_set[0]) { + return false; + } + if (!this->angle_is_set[1]) { + return true; + } + return this->angle[0] <= this->angle[1]; + } + + bool first_angle_is_largest() const + { + if (!this->angle_is_set[0]) { + return false; + } + if (!this->angle_is_set[1]) { + return true; + } + return this->angle[0] > this->angle[1]; + } +}; + +/* Fill edge segment. An edge segment is a set of points on an existing curve. A fill edge is a + * closed set of edge segments. */ +struct EdgeSegment { + /* Curve index in #Curves2DSpace struct (with curve points in 2D space). */ + int curve_index_2d; + /* Curve point index range. */ + int2 point_range; + /* Segment flags. */ + gfFlag flag = gfFlag::None; + + /* Segment endings: intersection data or gap closure data. */ + Vector segment_ends; + /* Last handled segment end. */ + int segment_ends_index; + + void init_point_range(const int point_i) + { + this->point_range[0] = point_i; + this->point_range[1] = point_i; + } + + void set_point_range(const int point_i, const int direction) + { + if (direction == -1) { + this->point_range[1] = this->point_range[0]; + this->point_range[0] = point_i; + } + else { + this->point_range[1] = point_i; + } + } +}; + +/* Intersecting curve. */ +struct IntersectingCurve { + /* Curve index in #Curves2DSpace struct. */ + int curve_index_2d; + /* Start point of the intersection (relative index: 0..). */ + int point_start; + /* End point of the intersection (relative index: 0..). */ + int point_end; + /* Distance of the intersection point on the intersected and intersecting segment. */ + float2 distance; + /* Intersection with end extension. */ + bool with_end_extension = false; + /* Index of the curve end extension. */ + int extension_index; + /* Curve has stroke material. */ + bool has_stroke = true; +}; + +/* Curve segment that overlaps with another curve segment. An overlap is a series of consecutive + * points in a curve that have exactly the same position as the points in the other curve. */ +struct OverlappingSegment { + int2 base_point_range; + int overlapping_curve_index; + int2 overlapping_point_range; + bool overlapping_backwards; +}; + +} // namespace blender::ed::greasepencil::fill diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_ops.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_ops.cc index b683d686317..fe900b15f35 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_ops.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_ops.cc @@ -81,6 +81,17 @@ bool grease_pencil_painting_poll(bContext *C) return true; } +bool grease_pencil_painting_fill_poll(bContext *C) +{ + if (!grease_pencil_painting_poll(C)) { + return false; + } + + /* FIXME: Can we avoid explicitly checking the tool here? */ + ToolSettings *ts = CTX_data_tool_settings(C); + return (ts->gp_paint->paint.brush->gpencil_tool == GPAINT_TOOL_FILL); +} + static void keymap_grease_pencil_edit_mode(wmKeyConfig *keyconf) { wmKeyMap *keymap = WM_keymap_ensure( @@ -95,6 +106,13 @@ static void keymap_grease_pencil_paint_mode(wmKeyConfig *keyconf) keymap->poll = grease_pencil_painting_poll; } +static void keymap_grease_pencil_painting_fill(wmKeyConfig *keyconf) +{ + wmKeyMap *keymap = WM_keymap_ensure( + keyconf, "Grease Pencil Paint Mode (Fill)", SPACE_EMPTY, RGN_TYPE_WINDOW); + keymap->poll = grease_pencil_painting_fill_poll; +} + } // namespace blender::ed::greasepencil void ED_operatortypes_grease_pencil() @@ -105,6 +123,7 @@ void ED_operatortypes_grease_pencil() ED_operatortypes_grease_pencil_select(); ED_operatortypes_grease_pencil_edit(); ED_operatortypes_grease_pencil_material(); + ED_operatortypes_grease_pencil_fill(); } void ED_operatormacros_grease_pencil() @@ -137,4 +156,5 @@ void ED_keymap_grease_pencil(wmKeyConfig *keyconf) using namespace blender::ed::greasepencil; keymap_grease_pencil_edit_mode(keyconf); keymap_grease_pencil_paint_mode(keyconf); + keymap_grease_pencil_painting_fill(keyconf); } diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_utils.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_utils.cc index 310ffa5afa9..41074c145ed 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_utils.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_utils.cc @@ -24,6 +24,8 @@ #include "DNA_scene_types.h" #include "DNA_view3d_types.h" +#include "DEG_depsgraph_query.hh" + #include "ED_curves.hh" #include "ED_grease_pencil.hh" #include "ED_view3d.hh" @@ -230,9 +232,9 @@ static std::pair get_minmax_selected_frame_numbers(const GreasePencil return std::pair(frame_min, frame_max); } -static Array get_frame_numbers_for_layer(const bke::greasepencil::Layer &layer, - const int current_frame, - const bool use_multi_frame_editing) +Array get_frame_numbers_for_layer(const bke::greasepencil::Layer &layer, + const int current_frame, + const bool use_multi_frame_editing) { Vector frame_numbers; if (use_multi_frame_editing) { @@ -606,4 +608,104 @@ IndexMask retrieve_editable_and_selected_elements(Object &object, return {}; } +Curves2DSpace curves_in_2d_space_get(Object &object, + const ViewContext &vc, + const GreasePencil &grease_pencil, + const Span drawings, + const Span layer_index, + const int frame_number, + const bool get_stroke_flag) +{ + const Object *ob_eval = DEG_get_evaluated_object(vc.depsgraph, &object); + + /* Count total number of editable curves and points in given Grease Pencil drawings. */ + Curves2DSpace cv2d; + Vector curve_point_offset; + int curve_num = 0, point_num = 0; + + for (const auto &drawing : drawings) { + cv2d.drawings.append(drawing); + cv2d.curve_offset.append(curve_num); + curve_point_offset.append(point_num); + + curve_num += drawing->geometry.curve_num; + point_num += drawing->geometry.point_num; + } + + /* Initialize the contiguous arrays for the 2D curve data. */ + cv2d.drawing_index_2d = Array(curve_num); + cv2d.is_cyclic = Array(curve_num); + cv2d.point_offset = Array(curve_num); + cv2d.point_size = Array(curve_num); + cv2d.curve_bbox = Array(curve_num); + cv2d.points_2d = Array(point_num); + if (get_stroke_flag) { + cv2d.has_stroke = Array(curve_num); + } + + /* Loop all drawings. */ + threading::parallel_for(cv2d.drawings.index_range(), 1, [&](const IndexRange range) { + for (const int drawing_i : range) { + /* Get deformed geomtry. */ + const bke::CurvesGeometry &curves = cv2d.drawings[drawing_i]->strokes(); + const OffsetIndices points_by_curve = curves.points_by_curve(); + const VArray cyclic = curves.cyclic(); + const bke::greasepencil::Layer &layer = *grease_pencil.layers()[layer_index[drawing_i]]; + const float4x4 layer_to_world = layer.to_world_space(*ob_eval); + const float4x4 projection = ED_view3d_ob_project_mat_get_from_obmat(vc.rv3d, layer_to_world); + const bke::crazyspace::GeometryDeformation deformation = + bke::crazyspace::get_evaluated_grease_pencil_drawing_deformation( + ob_eval, object, layer_index[drawing_i], frame_number); + + /* Get the curve materials. */ + VArray materials; + if (get_stroke_flag) { + materials = *curves.attributes().lookup_or_default( + "material_index", bke::AttrDomain::Curve, 0); + } + + /* Get the initial point index in the contiguous 2D point array for the curves in this + * drawing. */ + int point_contiguous = curve_point_offset[drawing_i]; + + /* Loop all curves. */ + for (const int curve_i : curves.curves_range()) { + const int curve_contiguous = cv2d.curve_offset[drawing_i] + curve_i; + const IndexRange points = points_by_curve[curve_i]; + + /* Set curve data. */ + BLI_rctf_init_minmax(&cv2d.curve_bbox[curve_contiguous]); + cv2d.point_size[curve_contiguous] = points.size(); + cv2d.point_offset[curve_contiguous] = point_contiguous; + cv2d.is_cyclic[curve_contiguous] = cyclic[curve_i]; + cv2d.drawing_index_2d[curve_contiguous] = drawing_i; + + /* Set stroke flag: true when the stroke property is active in the curve material. */ + if (get_stroke_flag) { + Material *material = BKE_object_material_get(&object, materials[curve_i] + 1); + cv2d.has_stroke[curve_contiguous] = (material && (material->gp_style->flag & + GP_MATERIAL_STROKE_SHOW) != 0); + } + + /* Loop all stroke points. */ + for (const int point_i : points) { + /* Convert point to 2D. */ + const float2 pos = ED_view3d_project_float_v2_m4( + vc.region, deformation.positions[point_i], projection); + + /* Store 2D point in contiguous array. */ + cv2d.points_2d[point_contiguous] = pos; + + /* Update stroke bounding box. */ + BLI_rctf_do_minmax_v(&cv2d.curve_bbox[curve_contiguous], pos); + + point_contiguous++; + } + } + } + }); + + return cv2d; +} + } // namespace blender::ed::greasepencil diff --git a/source/blender/editors/include/ED_grease_pencil.hh b/source/blender/editors/include/ED_grease_pencil.hh index db2e62493e2..192d47d301f 100644 --- a/source/blender/editors/include/ED_grease_pencil.hh +++ b/source/blender/editors/include/ED_grease_pencil.hh @@ -13,12 +13,14 @@ #include "BLI_generic_span.hh" #include "BLI_index_mask.hh" #include "BLI_math_matrix_types.hh" +#include "BLI_rect.h" #include "ED_keyframes_edit.hh" #include "WM_api.hh" struct bContext; +struct GreasePencilDrawing; struct Main; struct Object; struct KeyframeEditData; @@ -27,6 +29,7 @@ struct wmOperator; struct ToolSettings; struct Scene; struct UndoType; +struct ViewContext; struct ViewDepths; struct View3D; namespace blender { @@ -51,6 +54,7 @@ void ED_operatortypes_grease_pencil_layers(); void ED_operatortypes_grease_pencil_select(); void ED_operatortypes_grease_pencil_edit(); void ED_operatortypes_grease_pencil_material(); +void ED_operatortypes_grease_pencil_fill(); void ED_operatormacros_grease_pencil(); void ED_keymap_grease_pencil(wmKeyConfig *keyconf); @@ -173,6 +177,7 @@ bool editable_grease_pencil_poll(bContext *C); bool active_grease_pencil_layer_poll(bContext *C); bool editable_grease_pencil_point_selection_poll(bContext *C); bool grease_pencil_painting_poll(bContext *C); +bool grease_pencil_painting_fill_poll(bContext *C); struct DrawingInfo { const bke::greasepencil::Drawing &drawing; @@ -185,6 +190,9 @@ struct MutableDrawingInfo { const int frame_number; const float multi_frame_falloff; }; +Array get_frame_numbers_for_layer(const bke::greasepencil::Layer &layer, + const int current_frame, + const bool use_multi_frame_editing); Vector retrieve_editable_drawings(const Scene &scene, GreasePencil &grease_pencil); Vector retrieve_editable_drawings_with_falloff(const Scene &scene, @@ -244,4 +252,47 @@ IndexMask polyline_detect_corners(Span points, float angle_threshold, IndexMaskMemory &memory); +/** + * Structure for the accumulated curves of a set of drawings (layers) converted to + * viewport 2D space. + * All points of all curves of all drawings are put in one contiguous array. Curve points are + * accessible by offset indices. E.g. curve no. 7 of drawing no. 3 can be accessed by: + * curve_contiguous = curve_offset[3 (= drawing index)] + 7 (= curve index) + * point_contiguous = point_offset[curve_contiguous] + * first_point_of_curve = points_2d[point_contiguous] + */ +struct Curves2DSpace { + Vector drawings; + /* Curve offset for each drawing (layer). So when there are three drawings with 12, 15 and 8 + * curves, the curve offsets will be [0, 12, 27 (= 12 + 15)]. */ + Vector curve_offset; + /* Point index offset for each curve. */ + Array point_offset; + /* Number of points in each curve. */ + Array point_size; + /* Cyclic flag for each curve. */ + Array is_cyclic; + /* Material stroke flag for each curve. */ + Array has_stroke; + /* Index reference to `drawings` for each curve. */ + Array drawing_index_2d; + /* Contiguous array with all point positions in 2D space. */ + Array points_2d; + /* Bounding box of each curve in 2D space. */ + Array curve_bbox; +}; + +/** + * Convert all given Grease Pencil drawings to viewport 2D space. + * + * \return A struct with the 2D representation of all editable strokes. + */ +Curves2DSpace curves_in_2d_space_get(Object &object, + const ViewContext &vc, + const GreasePencil &grease_pencil, + const Span drawings, + const Span layer_index, + const int frame_number, + const bool get_stroke_flag = false); + } // namespace blender::ed::greasepencil diff --git a/source/blender/editors/space_view3d/space_view3d.cc b/source/blender/editors/space_view3d/space_view3d.cc index 249419cfeb8..80aeb063923 100644 --- a/source/blender/editors/space_view3d/space_view3d.cc +++ b/source/blender/editors/space_view3d/space_view3d.cc @@ -405,6 +405,10 @@ static void view3d_main_region_init(wmWindowManager *wm, ARegion *region) wm->defaultconf, "Grease Pencil Edit Mode", SPACE_EMPTY, RGN_TYPE_WINDOW); WM_event_add_keymap_handler(®ion->handlers, keymap); + keymap = WM_keymap_ensure( + wm->defaultconf, "Grease Pencil Paint Mode (Fill)", SPACE_EMPTY, RGN_TYPE_WINDOW); + WM_event_add_keymap_handler(®ion->handlers, keymap); + keymap = WM_keymap_ensure( wm->defaultconf, "Grease Pencil Paint Mode", SPACE_EMPTY, RGN_TYPE_WINDOW); WM_event_add_keymap_handler(®ion->handlers, keymap); diff --git a/source/blender/makesdna/DNA_brush_enums.h b/source/blender/makesdna/DNA_brush_enums.h index 034562c12dd..130750c30c4 100644 --- a/source/blender/makesdna/DNA_brush_enums.h +++ b/source/blender/makesdna/DNA_brush_enums.h @@ -128,6 +128,12 @@ typedef enum eGPDbrush_Flag2 { GP_BRUSH_USE_UV_RAND_PRESS = (1 << 11), } eGPDbrush_Flag2; +/* BrushGpencilSettings->fill_mode */ +typedef enum eGP_FillModes { + GP_FILL_MODE_FLOOD = 0, + GP_FILL_MODE_GEOMETRY = 1, +} eGP_FillModes; + /* BrushGpencilSettings->fill_draw_mode */ typedef enum eGP_FillDrawModes { GP_FILL_DMODE_BOTH = 0, diff --git a/source/blender/makesdna/DNA_brush_types.h b/source/blender/makesdna/DNA_brush_types.h index 069ba23103f..d7b78351131 100644 --- a/source/blender/makesdna/DNA_brush_types.h +++ b/source/blender/makesdna/DNA_brush_types.h @@ -116,10 +116,14 @@ typedef struct BrushGpencilSettings { /** Randomness for Value. */ float random_value; + /** Fill method: flood fill or geometry fill. */ + int fill_mode; /** Factor to extend stroke extremes using fill tool. */ float fill_extend_fac; /** Number of pixels to dilate fill area. */ int dilate_pixels; + /** Distance of curves to be considered in proximity (pixels). */ + int fill_proximity_distance; struct CurveMapping *curve_sensitivity; struct CurveMapping *curve_strength; diff --git a/source/blender/makesrna/intern/rna_brush.cc b/source/blender/makesrna/intern/rna_brush.cc index 9e47b08d27b..d0db7dcd2b5 100644 --- a/source/blender/makesrna/intern/rna_brush.cc +++ b/source/blender/makesrna/intern/rna_brush.cc @@ -364,6 +364,15 @@ static EnumPropertyItem rna_enum_gpencil_brush_eraser_modes_items[] = { {0, nullptr, 0, nullptr, nullptr}, }; +static EnumPropertyItem rna_enum_gpencil_fill_modes_items[] = { + {GP_FILL_MODE_FLOOD, "FLOOD", 0, "Flood Fill", "Use the raster-based flood fill algorithm"}, + {GP_FILL_MODE_GEOMETRY, + "GEOMETRY", + 0, + "Geometry Fill", + "Use the existing stroke geometry to create fills"}, + {0, nullptr, 0, nullptr, nullptr}}; + static EnumPropertyItem rna_enum_gpencil_fill_draw_modes_items[] = { {GP_FILL_DMODE_BOTH, "BOTH", @@ -1821,6 +1830,19 @@ static void rna_def_gpencil_options(BlenderRNA *brna) RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); RNA_def_property_update(prop, NC_GPENCIL | ND_DATA, nullptr); + /* Number of pixels to consider curves to be in proximity - used for gap closure in the + * fill tool. */ + prop = RNA_def_property(srna, "fill_proximity_distance", PROP_INT, PROP_PIXEL); + RNA_def_property_int_sdna(prop, nullptr, "fill_proximity_distance"); + RNA_def_property_range(prop, 0, 100); + RNA_def_property_int_default(prop, 0); + RNA_def_property_ui_text( + prop, + "By Proximity", + "Close gaps when strokes are less than this distance apart. Use zero to disable"); + RNA_def_parameter_clear_flags(prop, PROP_ANIMATABLE, ParameterFlag(0)); + RNA_def_property_update(prop, NC_GPENCIL | ND_DATA, nullptr); + /* Factor to determine outline external perimeter thickness. */ prop = RNA_def_property(srna, "outline_thickness_factor", PROP_FLOAT, PROP_FACTOR); RNA_def_property_float_sdna(prop, nullptr, "outline_fac"); @@ -1961,6 +1983,12 @@ static void rna_def_gpencil_options(BlenderRNA *brna) RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_GPENCIL); RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + prop = RNA_def_property(srna, "fill_mode", PROP_ENUM, PROP_NONE); + RNA_def_property_enum_sdna(prop, nullptr, "fill_mode"); + RNA_def_property_enum_items(prop, rna_enum_gpencil_fill_modes_items); + RNA_def_property_ui_text(prop, "Method", "Method to create fills"); + RNA_def_property_clear_flag(prop, PROP_ANIMATABLE); + prop = RNA_def_property(srna, "fill_draw_mode", PROP_ENUM, PROP_NONE); RNA_def_property_enum_sdna(prop, nullptr, "fill_draw_mode"); RNA_def_property_enum_items(prop, rna_enum_gpencil_fill_draw_modes_items);