From 7a3cd753bab557cf5c186826099f337fdd01eec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20T=C3=B6nne?= Date: Wed, 15 May 2024 13:36:06 +0200 Subject: [PATCH] GPv3: Fill tool to generate strokes in empty areas Implementation of the GPv2 Fill tool in Grease Pencil 3. This tool creates new strokes where the user clicks, by rendering strokes into an image and then performing flood-fill and boundary search operations. This is a minimal first part, several features of the GPv2 tool are still missing (gap filling methods, smoothing, dilate/erode). Co-authored-by: Falk David Pull Request: https://projects.blender.org/blender/blender/pulls/120693 --- .../bl_keymap_utils/keymap_from_toolbar.py | 1 + .../keyconfig/keymap_data/blender_default.py | 88 +- .../startup/bl_ui/properties_paint_common.py | 94 +- .../startup/bl_ui/space_toolsystem_toolbar.py | 10 + scripts/startup/bl_ui/space_view3d.py | 2 +- scripts/startup/bl_ui/space_view3d_toolbar.py | 3 +- .../editors/grease_pencil/CMakeLists.txt | 1 + .../intern/grease_pencil_image_render.cc | 419 +++++++ .../grease_pencil/intern/grease_pencil_ops.cc | 48 + .../editors/include/ED_grease_pencil.hh | 137 +++ .../editors/sculpt_paint/CMakeLists.txt | 1 + .../sculpt_paint/grease_pencil_draw_ops.cc | 635 +++++++++- .../sculpt_paint/grease_pencil_fill.cc | 1041 +++++++++++++++++ .../editors/space_view3d/space_view3d.cc | 8 + 14 files changed, 2440 insertions(+), 48 deletions(-) create mode 100644 source/blender/editors/grease_pencil/intern/grease_pencil_image_render.cc create mode 100644 source/blender/editors/sculpt_paint/grease_pencil_fill.cc diff --git a/scripts/modules/bl_keymap_utils/keymap_from_toolbar.py b/scripts/modules/bl_keymap_utils/keymap_from_toolbar.py index 1f5f0ee17b0..230d8f14ded 100644 --- a/scripts/modules/bl_keymap_utils/keymap_from_toolbar.py +++ b/scripts/modules/bl_keymap_utils/keymap_from_toolbar.py @@ -191,6 +191,7 @@ def generate(context, space_type, *, use_fallback_keys=True, use_reset=True): 'SCULPT_GPENCIL': "gpencil_sculpt_tool", 'WEIGHT_GPENCIL': "gpencil_weight_tool", 'SCULPT_CURVES': "curves_sculpt_tool", + 'PAINT_GREASE_PENCIL': "gpencil_tool", }.get(mode, None) else: attr = None diff --git a/scripts/presets/keyconfig/keymap_data/blender_default.py b/scripts/presets/keyconfig/keymap_data/blender_default.py index 7a8947b3e3b..a07641c6050 100644 --- a/scripts/presets/keyconfig/keymap_data/blender_default.py +++ b/scripts/presets/keyconfig/keymap_data/blender_default.py @@ -4567,16 +4567,6 @@ def km_grease_pencil_paint_mode(_params): ) items.extend([ - ("brush.scale_size", {"type": 'LEFT_BRACKET', "value": 'PRESS', "repeat": True}, - {"properties": [("scalar", 0.9)]}), - ("brush.scale_size", {"type": 'RIGHT_BRACKET', "value": 'PRESS', "repeat": True}, - {"properties": [("scalar", 1.0 / 0.9)]}), - ("grease_pencil.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), - ("grease_pencil.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, - {"properties": [("mode", 'INVERT')]}), - ("grease_pencil.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True}, - {"properties": [("mode", 'SMOOTH')]}), - *_template_paint_radial_control("gpencil_paint"), # Active material op_menu("VIEW3D_MT_greasepencil_material_active", {"type": 'U', "value": 'PRESS'}), # Active layer @@ -4594,6 +4584,30 @@ def km_grease_pencil_paint_mode(_params): return keymap +def km_grease_pencil_brush_stroke(_params): + items = [] + keymap = ( + "Grease Pencil Brush Stroke", + {"space_type": 'EMPTY', "region_type": 'WINDOW'}, + {"items": items}, + ) + + items.extend([ + ("brush.scale_size", {"type": 'LEFT_BRACKET', "value": 'PRESS', "repeat": True}, + {"properties": [("scalar", 0.9)]}), + ("brush.scale_size", {"type": 'RIGHT_BRACKET', "value": 'PRESS', "repeat": True}, + {"properties": [("scalar", 1.0 / 0.9)]}), + ("grease_pencil.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), + ("grease_pencil.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, + {"properties": [("mode", 'INVERT')]}), + ("grease_pencil.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True}, + {"properties": [("mode", 'SMOOTH')]}), + *_template_paint_radial_control("gpencil_paint"), + ]) + + return keymap + + def km_grease_pencil_edit_mode(params): items = [] keymap = ( @@ -4764,6 +4778,57 @@ def km_grease_pencil_weight_paint(params): return keymap +# Grease Pencil v3 Fill Tool. +def km_grease_pencil_fill_tool(_params): + items = [] + keymap = ( + "Grease Pencil Fill Tool", + {"space_type": 'EMPTY', "region_type": 'WINDOW'}, + {"items": items}, + ) + + items.extend([ + # Fill operator. + ("grease_pencil.fill", {"type": 'LEFTMOUSE', "value": 'PRESS'}, + {"properties": [("on_back", False)]}), + ("grease_pencil.fill", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, + {"properties": [("on_back", False)]}), + # Use regular stroke operator when holding shift to draw lines. + ("grease_pencil.brush_stroke", {"type": 'LEFTMOUSE', "value": 'PRESS', "shift": True}, + None), + ]) + + return keymap + + +def km_grease_pencil_fill_tool_modal_map(params): + items = [] + keymap = ( + "Fill Tool Modal Map", + {"space_type": 'EMPTY', "region_type": 'WINDOW', "modal": True}, + {"items": items}, + ) + + items.extend([ + ("CANCEL", {"type": 'ESC', "value": 'PRESS', "any": True}, None), + ("CANCEL", {"type": 'RIGHTMOUSE', "value": 'PRESS'}, None), + ("CONFIRM", {"type": 'LEFTMOUSE', "value": 'PRESS', "any": True}, None), + ("GAP_CLOSURE_MODE", {"type": 'S', "value": 'PRESS'}, None), + ("EXTENSIONS_LENGTHEN", {"type": 'PAGE_UP', "value": 'PRESS', "repeat": True}, None), + ("EXTENSIONS_LENGTHEN", {"type": 'WHEELUPMOUSE', "value": 'PRESS'}, None), + ("EXTENSIONS_SHORTEN", {"type": 'PAGE_DOWN', "value": 'PRESS', "repeat": True}, None), + ("EXTENSIONS_SHORTEN", {"type": 'WHEELDOWNMOUSE', "value": 'PRESS'}, None), + ("EXTENSIONS_DRAG", {"type": 'MIDDLEMOUSE', "value": 'PRESS'}, None), + ("EXTENSIONS_COLLIDE", {"type": 'D', "value": 'PRESS'}, None), + ("INVERT", {"type": 'LEFT_CTRL', "value": 'PRESS', "any": True}, None), + ("INVERT", {"type": 'RIGHT_CTRL', "value": 'PRESS', "any": True}, None), + ("PRECISION", {"type": 'LEFT_SHIFT', "value": 'PRESS', "any": True}, None), + ("PRECISION", {"type": 'RIGHT_SHIFT', "value": 'PRESS', "any": True}, None), + ]) + + return keymap + + # ------------------------------------------------------------------------------ # Object/Pose Modes @@ -8926,6 +8991,8 @@ def generate_keymaps(params=None): km_grease_pencil_edit_mode(params), km_grease_pencil_sculpt_mode(params), km_grease_pencil_weight_paint(params), + km_grease_pencil_brush_stroke(params), + km_grease_pencil_fill_tool(params), # Object mode. km_object_mode(params), km_object_non_modal(params), @@ -8977,6 +9044,7 @@ def generate_keymaps(params=None): km_curve_pen_modal_map(params), km_node_link_modal_map(params), km_grease_pencil_primitive_tool_modal_map(params), + km_grease_pencil_fill_tool_modal_map(params), # Gizmos. km_generic_gizmo(params), diff --git a/scripts/startup/bl_ui/properties_paint_common.py b/scripts/startup/bl_ui/properties_paint_common.py index f25b5c23691..f469a401a97 100644 --- a/scripts/startup/bl_ui/properties_paint_common.py +++ b/scripts/startup/bl_ui/properties_paint_common.py @@ -1452,41 +1452,49 @@ def brush_basic_grease_pencil_paint_settings(layout, context, brush, *, compact= grease_pencil_tool = brush.gpencil_tool - size = "size" - size_owner = ups if ups.use_unified_size else brush - if size_owner.use_locked_size == 'SCENE': - size = "unprojected_radius" + if grease_pencil_tool in {'DRAW', 'ERASE', 'TINT'} or tool.idname in { + "builtin.arc", + "builtin.curve", + "builtin.line", + "builtin.box", + "builtin.circle", + "builtin.polyline", + }: + size = "size" + size_owner = ups if ups.use_unified_size else brush + if size_owner.use_locked_size == 'SCENE': + size = "unprojected_radius" - UnifiedPaintPanel.prop_unified( - layout, - context, - brush, - size, - unified_name="use_unified_size", - pressure_name="use_pressure_size", - text="Radius", - slider=True, - header=compact, - ) + 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() - col.template_curve_mapping(gp_settings, "curve_strength", brush=True, use_negative_slope=True) + if brush.use_pressure_strength and not compact: + col = layout.column() + col.template_curve_mapping(gp_settings, "curve_strength", brush=True, use_negative_slope=True) # Brush details if tool.idname in { @@ -1520,6 +1528,30 @@ def brush_basic_grease_pencil_paint_settings(layout, context, brush, *, compact= if settings.use_thickness_curve: # Pressure curve. layout.template_curve_mapping(settings, "thickness_primitive_curve", brush=True) + elif grease_pencil_tool == 'DRAW': + layout.prop(gp_settings, "active_smooth_factor") + row = layout.row(align=True) + if compact: + row.prop(gp_settings, "caps_type", text="", expand=True) + else: + row.prop(gp_settings, "caps_type", text="Caps Type") + elif brush.gpencil_tool == 'FILL': + use_property_split_prev = layout.use_property_split + if compact: + row = layout.row(align=True) + row.prop(gp_settings, "fill_direction", text="", expand=True) + else: + layout.use_property_split = False + row = layout.row(align=True) + row.prop(gp_settings, "fill_direction", expand=True) + + row = layout.row(align=True) + row.prop(gp_settings, "fill_factor") + row = layout.row(align=True) + row.prop(gp_settings, "dilate") + row = layout.row(align=True) + row.prop(brush, "size", text="Thickness") + layout.use_property_split = use_property_split_prev elif grease_pencil_tool == 'ERASE': layout.prop(gp_settings, "eraser_mode", expand=True) if gp_settings.eraser_mode == 'HARD': diff --git a/scripts/startup/bl_ui/space_toolsystem_toolbar.py b/scripts/startup/bl_ui/space_toolsystem_toolbar.py index 16f30257702..167ba3967b9 100644 --- a/scripts/startup/bl_ui/space_toolsystem_toolbar.py +++ b/scripts/startup/bl_ui/space_toolsystem_toolbar.py @@ -1875,6 +1875,15 @@ class _defs_grease_pencil_paint: 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( @@ -3501,6 +3510,7 @@ class VIEW3D_PT_tools_active(ToolSelectPanelHelper, Panel): _defs_view3d_generic.cursor, None, _defs_grease_pencil_paint.draw, + _defs_grease_pencil_paint.fill, _defs_grease_pencil_paint.erase, _defs_grease_pencil_paint.cutter, _defs_grease_pencil_paint.tint, diff --git a/scripts/startup/bl_ui/space_view3d.py b/scripts/startup/bl_ui/space_view3d.py index 7f03627f673..61555497648 100644 --- a/scripts/startup/bl_ui/space_view3d.py +++ b/scripts/startup/bl_ui/space_view3d.py @@ -702,7 +702,7 @@ class _draw_tool_settings_context_mode: 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 ec22f3d122a..37858af3a20 100644 --- a/scripts/startup/bl_ui/space_view3d_toolbar.py +++ b/scripts/startup/bl_ui/space_view3d_toolbar.py @@ -2586,7 +2586,7 @@ class VIEW3D_PT_tools_grease_pencil_v3_brush_settings(Panel, View3DPanel, Grease class VIEW3D_PT_tools_grease_pencil_v3_brush_advanced(View3DPanel, Panel): bl_context = ".greasepencil_paint" bl_label = "Advanced" - bl_parent_id = "VIEW3D_PT_tools_grease_pencil_brush_settings" + bl_parent_id = "VIEW3D_PT_tools_grease_pencil_v3_brush_settings" bl_category = "Tool" bl_options = {'DEFAULT_CLOSED'} bl_ui_units_x = 13 @@ -2642,7 +2642,6 @@ class VIEW3D_PT_tools_grease_pencil_v3_brush_advanced(View3DPanel, Panel): if ma and ma.grease_pencil.mode == 'LINE': subcol.enabled = False subcol.prop(gp_settings, "aspect") - elif brush.gpencil_tool == 'FILL': row = col.row(align=True) row.prop(gp_settings, "fill_draw_mode", text="Boundary", text_ctxt=i18n_contexts.id_gpencil) diff --git a/source/blender/editors/grease_pencil/CMakeLists.txt b/source/blender/editors/grease_pencil/CMakeLists.txt index 4274e1c456b..436bdae8d74 100644 --- a/source/blender/editors/grease_pencil/CMakeLists.txt +++ b/source/blender/editors/grease_pencil/CMakeLists.txt @@ -27,6 +27,7 @@ set(SRC intern/grease_pencil_edit.cc intern/grease_pencil_frames.cc intern/grease_pencil_geom.cc + intern/grease_pencil_image_render.cc intern/grease_pencil_layers.cc intern/grease_pencil_material.cc intern/grease_pencil_ops.cc diff --git a/source/blender/editors/grease_pencil/intern/grease_pencil_image_render.cc b/source/blender/editors/grease_pencil/intern/grease_pencil_image_render.cc new file mode 100644 index 00000000000..b4cd1904c26 --- /dev/null +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_image_render.cc @@ -0,0 +1,419 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BKE_grease_pencil.hh" +#include "BLI_color.hh" +#include "BLI_math_matrix.hh" + +#include "BKE_attribute.hh" +#include "BKE_camera.h" +#include "BKE_curves.hh" +#include "BKE_image.h" + +#include "DNA_gpencil_legacy_types.h" +#include "DNA_material_types.h" +#include "DNA_object_types.h" +#include "DNA_scene_types.h" +#include "DNA_view3d_types.h" + +#include "ED_grease_pencil.hh" +#include "ED_view3d.hh" + +#include "IMB_imbuf.hh" +#include "IMB_imbuf_types.hh" + +#include "GPU_debug.hh" +#include "GPU_framebuffer.hh" +#include "GPU_immediate.hh" +#include "GPU_matrix.hh" +#include "GPU_shader_shared.hh" +#include "GPU_state.hh" +#include "GPU_texture.hh" +#include "GPU_vertex_format.hh" + +namespace blender::ed::greasepencil::image_render { + +/* Enable GPU debug capture (needs WITH_RENDERDOC option). */ +constexpr const bool enable_debug_gpu_capture = true; + +RegionViewData region_init(ARegion ®ion, const int2 &win_size) +{ + const RegionViewData data = {int2{region.winx, region.winy}, region.winrct}; + + /* Resize region. */ + region.winrct.xmin = 0; + region.winrct.ymin = 0; + region.winrct.xmax = win_size.x; + region.winrct.ymax = win_size.y; + region.winx = short(win_size.x); + region.winy = short(win_size.y); + + return data; +} + +void region_reset(ARegion ®ion, const RegionViewData &data) +{ + region.winx = data.region_winsize.x; + region.winy = data.region_winsize.y; + region.winrct = data.region_winrct; +} + +GPUOffScreen *image_render_begin(const int2 &win_size) +{ + if (enable_debug_gpu_capture) { + GPU_debug_capture_begin("Grease Pencil Image Render"); + } + + char err_out[256] = "unknown"; + GPUOffScreen *offscreen = GPU_offscreen_create( + win_size.x, win_size.y, true, GPU_RGBA8, GPU_TEXTURE_USAGE_HOST_READ, err_out); + if (offscreen == nullptr) { + return nullptr; + } + + GPU_offscreen_bind(offscreen, true); + + GPU_matrix_push_projection(); + GPU_matrix_identity_projection_set(); + GPU_matrix_push(); + GPU_matrix_identity_set(); + + GPU_clear_color(0.0f, 0.0f, 0.0f, 0.0f); + GPU_clear_depth(1.0f); + + return offscreen; +} + +Image *image_render_end(Main &bmain, GPUOffScreen *buffer) +{ + const int2 win_size = {GPU_offscreen_width(buffer), GPU_offscreen_height(buffer)}; + const uint imb_flag = IB_rect; + ImBuf *ibuf = IMB_allocImBuf(win_size.x, win_size.y, 32, imb_flag); + if (ibuf->float_buffer.data) { + GPU_offscreen_read_color(buffer, GPU_DATA_FLOAT, ibuf->float_buffer.data); + } + else if (ibuf->byte_buffer.data) { + GPU_offscreen_read_color(buffer, GPU_DATA_UBYTE, ibuf->byte_buffer.data); + } + if (ibuf->float_buffer.data && ibuf->byte_buffer.data) { + IMB_rect_from_float(ibuf); + } + + Image *ima = BKE_image_add_from_imbuf(&bmain, ibuf, "Grease Pencil Fill"); + ima->id.tag |= LIB_TAG_DOIT; + + BKE_image_release_ibuf(ima, ibuf, nullptr); + + /* Switch back to regular frame-buffer. */ + GPU_offscreen_unbind(buffer, true); + GPU_offscreen_free(buffer); + + if (enable_debug_gpu_capture) { + GPU_debug_capture_end(); + } + + return ima; +} + +void set_viewmat(const ViewContext &view_context, + const Scene &scene, + const int2 &win_size, + const float2 &zoom, + const float2 &offset) +{ + rctf viewplane; + float clip_start, clip_end; + const bool is_ortho = ED_view3d_viewplane_get(view_context.depsgraph, + view_context.v3d, + view_context.rv3d, + win_size.x, + win_size.y, + &viewplane, + &clip_start, + &clip_end, + nullptr); + + /* Rescale `viewplane` to fit all strokes. */ + const float2 view_min = float2(viewplane.xmin, viewplane.ymin); + const float2 view_max = float2(viewplane.xmax, viewplane.ymax); + const float2 view_extent = view_max - view_min; + const float2 view_center = 0.5f * (view_max + view_min); + const float2 offset_abs = offset * view_extent; + const float2 view_min_new = (view_min - view_center) * zoom + view_center + offset_abs; + const float2 view_max_new = (view_max - view_center) * zoom + view_center + offset_abs; + viewplane.xmin = view_min_new.x; + viewplane.ymin = view_min_new.y; + viewplane.xmax = view_max_new.x; + viewplane.ymax = view_max_new.y; + + float4x4 winmat; + if (is_ortho) { + orthographic_m4(winmat.ptr(), + viewplane.xmin, + viewplane.xmax, + viewplane.ymin, + viewplane.ymax, + -clip_end, + clip_end); + } + else { + perspective_m4(winmat.ptr(), + viewplane.xmin, + viewplane.xmax, + viewplane.ymin, + viewplane.ymax, + clip_start, + clip_end); + } + + ED_view3d_update_viewmat(view_context.depsgraph, + &scene, + view_context.v3d, + view_context.region, + nullptr, + winmat.ptr(), + nullptr, + true); + GPU_matrix_set(view_context.rv3d->viewmat); + GPU_matrix_projection_set(view_context.rv3d->winmat); +} + +void clear_viewmat() +{ + GPU_matrix_pop_projection(); + GPU_matrix_pop(); +} + +void draw_dot(const float3 &position, const float point_size, const ColorGeometry4f &color) +{ + GPUVertFormat *format = immVertexFormat(); + uint attr_pos = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 3, GPU_FETCH_FLOAT); + uint attr_size = GPU_vertformat_attr_add(format, "size", GPU_COMP_F32, 1, GPU_FETCH_FLOAT); + uint attr_color = GPU_vertformat_attr_add(format, "color", GPU_COMP_F32, 4, GPU_FETCH_FLOAT); + + GPU_program_point_size(true); + immBindBuiltinProgram(GPU_SHADER_3D_POINT_VARYING_SIZE_VARYING_COLOR); + immBegin(GPU_PRIM_POINTS, 1); + immAttr1f(attr_size, point_size * M_SQRT2); + immAttr4fv(attr_color, color); + immVertex3fv(attr_pos, position); + immEnd(); + immUnbindProgram(); + GPU_program_point_size(false); +} + +void draw_polyline(const IndexRange indices, + Span positions, + const VArray &colors, + const float4x4 &layer_to_world, + const bool cyclic, + const float line_width) +{ + GPUVertFormat *format = immVertexFormat(); + const uint attr_pos = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 3, GPU_FETCH_FLOAT); + const uint attr_color = GPU_vertformat_attr_add( + format, "color", GPU_COMP_F32, 4, GPU_FETCH_FLOAT); + immBindBuiltinProgram(GPU_SHADER_3D_FLAT_COLOR); + + GPU_line_width(line_width); + /* If cyclic the curve needs one more vertex. */ + const int cyclic_add = (cyclic && indices.size() > 2) ? 1 : 0; + immBeginAtMost(GPU_PRIM_LINE_STRIP, indices.size() + cyclic_add); + + for (const int point_i : indices) { + immAttr4fv(attr_color, colors[point_i]); + immVertex3fv(attr_pos, math::transform_point(layer_to_world, positions[point_i])); + } + + if (cyclic && indices.size() > 2) { + const int point_i = indices[0]; + immAttr4fv(attr_color, colors[point_i]); + immVertex3fv(attr_pos, math::transform_point(layer_to_world, positions[point_i])); + } + + immEnd(); + immUnbindProgram(); +} + +static GPUUniformBuf *create_shader_ubo(const RegionView3D &rv3d, + const int2 &win_size, + const Object &object, + const eGPDstroke_Caps cap_start, + const eGPDstroke_Caps cap_end, + const bool is_fill_stroke) +{ + GPencilStrokeData data; + copy_v2_v2(data.viewport, float2(win_size)); + data.pixsize = rv3d.pixsize; + data.objscale = math::average(float3(object.scale)); + /* TODO Was based on the GP_DATA_STROKE_KEEPTHICKNESS flag which is currently not converted. */ + data.keep_size = false; + data.pixfactor = 1.0f; + /* X-ray mode always to 3D space to avoid wrong Z-depth calculation (#60051). */ + data.xraymode = GP_XRAY_3DSPACE; + data.caps_start = cap_start; + data.caps_end = cap_end; + data.fill_stroke = is_fill_stroke; + + return GPU_uniformbuf_create_ex(sizeof(GPencilStrokeData), &data, __func__); +} + +void draw_grease_pencil_stroke(const RegionView3D &rv3d, + const int2 &win_size, + const Object &object, + const IndexRange indices, + Span positions, + const VArray &radii, + const VArray &colors, + const float4x4 &layer_to_world, + const bool cyclic, + const eGPDstroke_Caps cap_start, + const eGPDstroke_Caps cap_end, + const bool fill_stroke) +{ + if (indices.is_empty()) { + return; + } + + GPUVertFormat *format = immVertexFormat(); + const uint attr_pos = GPU_vertformat_attr_add(format, "pos", GPU_COMP_F32, 3, GPU_FETCH_FLOAT); + const uint attr_color = GPU_vertformat_attr_add( + format, "color", GPU_COMP_F32, 4, GPU_FETCH_FLOAT); + const uint attr_thickness = GPU_vertformat_attr_add( + format, "thickness", GPU_COMP_F32, 1, GPU_FETCH_FLOAT); + + immBindBuiltinProgram(GPU_SHADER_GPENCIL_STROKE); + GPUUniformBuf *ubo = create_shader_ubo(rv3d, win_size, object, cap_start, cap_end, fill_stroke); + immBindUniformBuf("gpencil_stroke_data", ubo); + + /* If cyclic the curve needs one more vertex. */ + const int cyclic_add = (cyclic && indices.size() > 2) ? 1 : 0; + + immBeginAtMost(GPU_PRIM_LINE_STRIP_ADJ, indices.size() + cyclic_add + 2); + + auto draw_point = [&](const int point_i) { + constexpr const float radius_to_pixel_factor = + 1.0f / bke::greasepencil::LEGACY_RADIUS_CONVERSION_FACTOR; + const float thickness = radii[point_i] * radius_to_pixel_factor; + constexpr const float min_thickness = 0.05f; + + immAttr4fv(attr_color, colors[point_i]); + immAttr1f(attr_thickness, std::max(thickness, min_thickness)); + immVertex3fv(attr_pos, math::transform_point(layer_to_world, positions[point_i])); + }; + + /* First point for adjacency (not drawn). */ + if (cyclic && indices.size() > 2) { + draw_point(indices.last() - 1); + } + else { + draw_point(indices.first() + 1); + } + + for (const int point_i : indices) { + draw_point(point_i); + } + + if (cyclic && indices.size() > 2) { + draw_point(indices.first()); + draw_point(indices.first() + 1); + } + /* Last adjacency point (not drawn). */ + else { + draw_point(indices.last() - 1); + } + + immEnd(); + immUnbindProgram(); + + GPU_uniformbuf_free(ubo); +} + +void draw_dots(const IndexRange indices, + Span positions, + const VArray &radii, + const VArray &colors, + const float4x4 &layer_to_world) +{ + /* TODO */ + UNUSED_VARS(indices, positions, radii, colors, layer_to_world); +} + +void draw_grease_pencil_strokes(const RegionView3D &rv3d, + const int2 &win_size, + const Object &object, + const bke::greasepencil::Drawing &drawing, + const IndexMask &strokes_mask, + const VArray &colors, + const float4x4 &layer_to_world, + const int mode, + const bool use_xray, + const bool fill_strokes) +{ + GPU_program_point_size(true); + + /* Do not write to depth (avoid self-occlusion). */ + const bool prev_depth_mask = GPU_depth_mask_get(); + GPU_depth_mask(false); + + const bke::CurvesGeometry &curves = drawing.strokes(); + const OffsetIndices points_by_curve = curves.points_by_curve(); + const Span positions = curves.positions(); + const bke::AttributeAccessor attributes = curves.attributes(); + const VArray cyclic = curves.cyclic(); + const VArray &radii = drawing.radii(); + const VArray stroke_start_caps = *attributes.lookup_or_default( + "start_cap", bke::AttrDomain::Curve, GP_STROKE_CAP_ROUND); + const VArray stroke_end_caps = *attributes.lookup_or_default( + "end_cap", bke::AttrDomain::Curve, GP_STROKE_CAP_ROUND); + + /* Note: Serial loop without GrainSize, since immediate mode drawing can't happen in worker + * threads, has to be from the main thread. */ + strokes_mask.foreach_index([&](const int stroke_i) { + const float stroke_radius = radii[stroke_i]; + if (stroke_radius <= 0) { + return; + } + + if (!use_xray) { + GPU_depth_test(GPU_DEPTH_LESS_EQUAL); + + /* First arg is normally rv3d->dist, but this isn't + * available here and seems to work quite well without. */ + GPU_polygon_offset(1.0f, 1.0f); + } + + switch (eMaterialGPencilStyle_Mode(mode)) { + case GP_MATERIAL_MODE_LINE: + draw_grease_pencil_stroke(rv3d, + win_size, + object, + points_by_curve[stroke_i], + positions, + radii, + colors, + layer_to_world, + cyclic[stroke_i], + eGPDstroke_Caps(stroke_start_caps[stroke_i]), + eGPDstroke_Caps(stroke_end_caps[stroke_i]), + fill_strokes); + break; + case GP_MATERIAL_MODE_DOT: + case GP_MATERIAL_MODE_SQUARE: + draw_dots(points_by_curve[stroke_i], positions, radii, colors, layer_to_world); + break; + } + + if (!use_xray) { + GPU_depth_test(GPU_DEPTH_NONE); + + GPU_polygon_offset(0.0f, 0.0f); + } + }); + + GPU_depth_mask(prev_depth_mask); + GPU_program_point_size(false); +} + +} // namespace blender::ed::greasepencil::image_render 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 f136e84661e..4faaf39abe2 100644 --- a/source/blender/editors/grease_pencil/intern/grease_pencil_ops.cc +++ b/source/blender/editors/grease_pencil/intern/grease_pencil_ops.cc @@ -7,7 +7,9 @@ */ #include "BKE_context.hh" +#include "BKE_paint.hh" +#include "DNA_brush_enums.h" #include "DNA_object_enums.h" #include "DNA_scene_types.h" @@ -15,6 +17,7 @@ #include "ED_screen.hh" #include "WM_api.hh" +#include "WM_toolsystem.hh" #include "WM_types.hh" #include "RNA_access.hh" @@ -142,6 +145,48 @@ static void keymap_grease_pencil_weight_paint_mode(wmKeyConfig *keyconf) keymap->poll = grease_pencil_weight_painting_poll; } +/* Enabled for all tools except the fill tool. */ +static bool keymap_grease_pencil_brush_stroke_poll(bContext *C) +{ + if (!grease_pencil_painting_poll(C)) { + return false; + } + if (!WM_toolsystem_active_tool_is_brush(C)) { + return false; + } + ToolSettings *ts = CTX_data_tool_settings(C); + Brush *brush = BKE_paint_brush(&ts->gp_paint->paint); + return brush && brush->gpencil_settings && brush->gpencil_tool != GPAINT_TOOL_FILL; +} + +static void keymap_grease_pencil_brush_stroke(wmKeyConfig *keyconf) +{ + wmKeyMap *keymap = WM_keymap_ensure( + keyconf, "Grease Pencil Brush Stroke", SPACE_EMPTY, RGN_TYPE_WINDOW); + keymap->poll = keymap_grease_pencil_brush_stroke_poll; +} + +/* Enabled only for the fill tool. */ +static bool keymap_grease_pencil_fill_tool_poll(bContext *C) +{ + if (!grease_pencil_painting_poll(C)) { + return false; + } + if (!WM_toolsystem_active_tool_is_brush(C)) { + return false; + } + ToolSettings *ts = CTX_data_tool_settings(C); + Brush *brush = BKE_paint_brush(&ts->gp_paint->paint); + return brush && brush->gpencil_settings && brush->gpencil_tool == GPAINT_TOOL_FILL; +} + +static void keymap_grease_pencil_fill_tool(wmKeyConfig *keyconf) +{ + wmKeyMap *keymap = WM_keymap_ensure( + keyconf, "Grease Pencil Fill Tool", SPACE_EMPTY, RGN_TYPE_WINDOW); + keymap->poll = keymap_grease_pencil_fill_tool_poll; +} + } // namespace blender::ed::greasepencil void ED_operatortypes_grease_pencil() @@ -198,5 +243,8 @@ void ED_keymap_grease_pencil(wmKeyConfig *keyconf) keymap_grease_pencil_paint_mode(keyconf); keymap_grease_pencil_sculpt_mode(keyconf); keymap_grease_pencil_weight_paint_mode(keyconf); + keymap_grease_pencil_brush_stroke(keyconf); + keymap_grease_pencil_fill_tool(keyconf); ED_primitivetool_modal_keymap(keyconf); + ED_filltool_modal_keymap(keyconf); } diff --git a/source/blender/editors/include/ED_grease_pencil.hh b/source/blender/editors/include/ED_grease_pencil.hh index 28b0eadf3ba..0e1197969d9 100644 --- a/source/blender/editors/include/ED_grease_pencil.hh +++ b/source/blender/editors/include/ED_grease_pencil.hh @@ -25,6 +25,7 @@ struct Object; struct KeyframeEditData; struct wmKeyConfig; struct wmOperator; +struct GPUOffScreen; struct ToolSettings; struct Scene; struct UndoType; @@ -59,6 +60,7 @@ void ED_operatortypes_grease_pencil_weight_paint(); void ED_operatormacros_grease_pencil(); void ED_keymap_grease_pencil(wmKeyConfig *keyconf); void ED_primitivetool_modal_keymap(wmKeyConfig *keyconf); +void ED_filltool_modal_keymap(wmKeyConfig *keyconf); void GREASE_PENCIL_OT_stroke_cutter(wmOperatorType *ot); @@ -400,4 +402,139 @@ IndexRange clipboard_paste_strokes(Main &bmain, bke::greasepencil::Drawing &drawing, bool paste_back); +/** + * Method used by the Fill tool to fit the render buffer to strokes. + */ +enum FillToolFitMethod { + /* Use the current view projection unchanged. */ + None, + /* Fit all strokes into the view (may change pixel size). */ + FitToView, +}; + +/** + * Fill tool for generating strokes in empty areas. + * + * This uses an approximate render of strokes and boundaries, + * then fills the image starting from the mouse position. + * The outlines of the filled pixel areas are returned as curves. + * + * \param layer: The layer containing the new stroke, used for reprojecting from images. + * \param boundary_layers: Layers that are purely for boundaries, regular strokes are not rendered. + * \param src_drawings: Drawings to include as boundary strokes. + * \param invert: Construct boundary around empty areas instead. + * \param fill_point: Point from which to start the bucket fill. + * \param fit_method: View fitting method to include all strokes. + * \param stroke_material_index: Material index to use for the new strokes. + * \param keep_images: Keep the image data block after generating curves. + */ +bke::CurvesGeometry fill_strokes(const ViewContext &view_context, + const Brush &brush, + const Scene &scene, + const bke::greasepencil::Layer &layer, + const VArray &boundary_layers, + Span src_drawings, + bool invert, + const float2 &fill_point, + FillToolFitMethod fit_method, + int stroke_material_index, + bool keep_images); + +namespace image_render { + +/** Region size to restore after rendering. */ +struct RegionViewData { + int2 region_winsize; + rcti region_winrct; +}; + +/** + * Set up region to match the render buffer size. + */ +RegionViewData region_init(ARegion ®ion, const int2 &win_size); +/** + * Restore original region size after rendering. + */ +void region_reset(ARegion ®ion, const RegionViewData &data); + +/** + * Create and offscreen buffer for rendering. + */ +GPUOffScreen *image_render_begin(const int2 &win_size); +/** + * Finish rendering and convert the offscreen buffer into an image. + */ +Image *image_render_end(Main &bmain, GPUOffScreen *buffer); + +/** + * Set up the view matrix for world space rendering. + * + * \param win_size: Size of the render window. + * \param zoom: Zoom factor to render a smaller or larger part of the view. + * \param offset: Offset of the view relative to the overall size. + */ +void set_viewmat(const ViewContext &view_context, + const Scene &scene, + const int2 &win_size, + const float2 &zoom, + const float2 &offset); +/** + * Reset the view matrix for screen space rendering. + */ +void clear_viewmat(); + +/** + * Draw a dot with a given size and color. + */ +void draw_dot(const float3 &position, float point_size, const ColorGeometry4f &color); +/** + * Draw a poly line from points. + */ +void draw_polyline(IndexRange indices, + Span positions, + const VArray &colors, + const float4x4 &layer_to_world, + bool cyclic, + float line_width); +/** + * Draw a curve using the Grease Pencil stroke shader. + */ +void draw_grease_pencil_stroke(const RegionView3D &rv3d, + const int2 &win_size, + const Object &object, + IndexRange indices, + Span positions, + const VArray &radii, + const VArray &colors, + const float4x4 &layer_to_world, + bool cyclic, + eGPDstroke_Caps cap_start, + eGPDstroke_Caps cap_end, + bool fill_stroke); +/** + * Draw points as quads or circles. + */ +void draw_dots(IndexRange indices, + Span positions, + const VArray &radii, + const VArray &colors, + const float4x4 &layer_to_world); + +/** + * Draw curves geometry. + * \param mode: Mode of \a eMaterialGPencilStyle_Mode. + */ +void draw_grease_pencil_strokes(const RegionView3D &rv3d, + const int2 &win_size, + const Object &object, + const bke::greasepencil::Drawing &drawing, + const IndexMask &strokes_mask, + const VArray &colors, + const float4x4 &layer_to_world, + int mode, + bool use_xray, + bool fill_strokes); + +} // namespace image_render + } // namespace blender::ed::greasepencil diff --git a/source/blender/editors/sculpt_paint/CMakeLists.txt b/source/blender/editors/sculpt_paint/CMakeLists.txt index 2ad34c5097f..5602e9aaeff 100644 --- a/source/blender/editors/sculpt_paint/CMakeLists.txt +++ b/source/blender/editors/sculpt_paint/CMakeLists.txt @@ -42,6 +42,7 @@ set(SRC curves_sculpt_snake_hook.cc grease_pencil_draw_ops.cc grease_pencil_erase.cc + grease_pencil_fill.cc grease_pencil_paint.cc grease_pencil_sculpt_clone.cc grease_pencil_sculpt_common.cc diff --git a/source/blender/editors/sculpt_paint/grease_pencil_draw_ops.cc b/source/blender/editors/sculpt_paint/grease_pencil_draw_ops.cc index e24375b634e..8c1a61fc841 100644 --- a/source/blender/editors/sculpt_paint/grease_pencil_draw_ops.cc +++ b/source/blender/editors/sculpt_paint/grease_pencil_draw_ops.cc @@ -2,35 +2,60 @@ * * SPDX-License-Identifier: GPL-2.0-or-later */ +#include "ANIM_keyframing.hh" + +#include "BKE_brush.hh" +#include "BKE_colortools.hh" #include "BKE_context.hh" +#include "BKE_curves.hh" #include "BKE_deform.hh" +#include "BKE_geometry_set.hh" #include "BKE_grease_pencil.hh" +#include "BKE_material.h" #include "BKE_object_deform.h" #include "BKE_paint.hh" #include "BKE_report.hh" +#include "BKE_screen.hh" -#include "DEG_depsgraph_query.hh" +#include "BLI_assert.h" +#include "BLI_math_vector.hh" +#include "BLI_rect.h" +#include "BLI_string.h" #include "DNA_brush_types.h" #include "DNA_grease_pencil_types.h" - #include "DNA_scene_types.h" +#include "DNA_view3d_types.h" +#include "DNA_windowmanager_types.h" + +#include "DEG_depsgraph_query.hh" + +#include "GEO_join_geometries.hh" + #include "ED_grease_pencil.hh" #include "ED_image.hh" #include "ED_object.hh" #include "ED_screen.hh" +#include "ED_view3d.hh" -#include "ANIM_keyframing.hh" +#include "MEM_guardedalloc.h" #include "RNA_access.hh" +#include "RNA_define.hh" + +#include "UI_interface.hh" + +#include "BLT_translation.hh" #include "WM_api.hh" #include "WM_message.hh" #include "WM_toolsystem.hh" +#include "WM_types.hh" #include "curves_sculpt_intern.hh" #include "grease_pencil_intern.hh" #include "paint_intern.hh" +#include "wm_event_types.hh" namespace blender::ed::sculpt_paint { @@ -123,7 +148,8 @@ static GreasePencilStrokeOperation *grease_pencil_brush_stroke_operation(bContex case GPAINT_TOOL_ERASE: return greasepencil::new_erase_operation().release(); case GPAINT_TOOL_FILL: - return nullptr; + /* Fill tool keymap uses the paint operator as alternative mode. */ + return greasepencil::new_paint_operation().release(); case GPAINT_TOOL_TINT: return greasepencil::new_tint_operation().release(); } @@ -464,6 +490,564 @@ static void GREASE_PENCIL_OT_weight_brush_stroke(wmOperatorType *ot) /** \} */ +/* -------------------------------------------------------------------- */ +/** \name Bucket Fill Operator + * \{ */ + +struct GreasePencilFillOpData { + blender::bke::greasepencil::Layer &layer; + + /* Brush properties, some of these are modified by modal keys. */ + int flag; + eGP_FillExtendModes fill_extend_mode; + float fill_extend_fac; + + int material_index; + + /* Mouse position where fill was initialized */ + float2 fill_mouse_pos; + /* Extension lines mode is enabled (middle mouse button). */ + bool is_extension_mode = false; + /* Mouse position where the extension mode was enabled. */ + float2 extension_mouse_pos; + /* Toggle inverse filling. */ + bool invert = false; + /* Toggle precision mode. */ + bool precision = false; + + ~GreasePencilFillOpData() + { + // TODO Remove drawing handler. + // if (this->draw_handle_3d) { + // ED_region_draw_cb_exit(this->region_type, this->draw_handle_3d); + // } + } + + static GreasePencilFillOpData from_context(bContext &C, + blender::bke::greasepencil::Layer &layer, + const int material_index) + { + using blender::bke::greasepencil::Layer; + + const ToolSettings &ts = *CTX_data_tool_settings(&C); + const Brush &brush = *BKE_paint_brush(&ts.gp_paint->paint); + + /* Enable custom drawing handlers to show help lines */ + const bool do_extend = (brush.gpencil_settings->flag & GP_BRUSH_FILL_SHOW_EXTENDLINES); + const bool help_lines = do_extend || + (brush.gpencil_settings->flag & GP_BRUSH_FILL_SHOW_HELPLINES); + if (help_lines) { + // TODO register 3D view overlay to render help lines. + // this->region_type = region.type; + // this->draw_handle_3d = ED_region_draw_cb_activate( + // region.type, grease_pencil_fill_draw_3d, tgpf, REGION_DRAW_POST_VIEW); + } + + return {layer, + brush.gpencil_settings->flag, + eGP_FillExtendModes(brush.gpencil_settings->fill_extend_mode), + brush.gpencil_settings->fill_extend_fac, + material_index}; + } +}; + +static void grease_pencil_fill_status_indicators(bContext &C, + const GreasePencilFillOpData &op_data) +{ + const bool is_extend = (op_data.fill_extend_mode == GP_FILL_EMODE_EXTEND); + const bool use_stroke_collide = (op_data.flag & GP_BRUSH_FILL_STROKE_COLLIDE) != 0; + const float fill_extend_fac = op_data.fill_extend_fac; + + char status_str[UI_MAX_DRAW_STR]; + BLI_snprintf(status_str, + sizeof(status_str), + IFACE_("Fill: ESC/RMB cancel, LMB Fill, Shift Draw on Back, MMB Adjust Extend, S: " + "Switch Mode, D: " + "Stroke Collision | %s %s (%.3f)"), + (is_extend) ? IFACE_("Extend") : IFACE_("Radius"), + (is_extend && use_stroke_collide) ? IFACE_("Stroke: ON") : IFACE_("Stroke: OFF"), + fill_extend_fac); + + ED_workspace_status_text(&C, status_str); +} + +static void grease_pencil_update_extend(bContext &C, const GreasePencilFillOpData &op_data) +{ + if (op_data.fill_extend_fac > 0.0f) { + // TODO update extension lines. + } + grease_pencil_fill_status_indicators(C, op_data); + WM_event_add_notifier(&C, NC_GPENCIL | NA_EDITED, nullptr); +} + +/* Layer mode defines layers where only marked boundary strokes are used. */ +static VArray get_fill_boundary_layers(const GreasePencil &grease_pencil, + eGP_FillLayerModes fill_layer_mode) +{ + BLI_assert(grease_pencil.has_active_layer()); + const IndexRange all_layers = grease_pencil.layers().index_range(); + const int active_layer_index = *grease_pencil.get_layer_index(*grease_pencil.get_active_layer()); + + switch (fill_layer_mode) { + case GP_FILL_GPLMODE_ACTIVE: + return VArray::ForFunc(all_layers.size(), [active_layer_index](const int index) { + return index != active_layer_index; + }); + case GP_FILL_GPLMODE_ABOVE: + return VArray::ForFunc(all_layers.size(), [active_layer_index](const int index) { + return index != active_layer_index + 1; + }); + case GP_FILL_GPLMODE_BELOW: + return VArray::ForFunc(all_layers.size(), [active_layer_index](const int index) { + return index != active_layer_index - 1; + }); + case GP_FILL_GPLMODE_ALL_ABOVE: + return VArray::ForFunc(all_layers.size(), [active_layer_index](const int index) { + return index <= active_layer_index; + }); + case GP_FILL_GPLMODE_ALL_BELOW: + return VArray::ForFunc(all_layers.size(), [active_layer_index](const int index) { + return index >= active_layer_index; + }); + case GP_FILL_GPLMODE_VISIBLE: + return VArray::ForFunc(all_layers.size(), [grease_pencil](const int index) { + return !grease_pencil.layers()[index]->is_visible(); + }); + } + return {}; +} + +/* Array of visible drawings to use as borders for generating a stroke in the editable drawing on + * the active layer. This is provided for every frame in the multi-frame edit range. */ +struct FillToolTargetInfo { + ed::greasepencil::MutableDrawingInfo target; + Vector sources; +}; + +static Vector ensure_editable_drawings(const Scene &scene, + GreasePencil &grease_pencil, + bke::greasepencil::Layer &target_layer) +{ + using namespace bke::greasepencil; + using ed::greasepencil::DrawingInfo; + using ed::greasepencil::MutableDrawingInfo; + + const ToolSettings *toolsettings = scene.toolsettings; + const bool use_multi_frame_editing = (toolsettings->gpencil_flags & + GP_USE_MULTI_FRAME_EDITING) != 0; + const bool use_autokey = blender::animrig::is_autokey_on(&scene); + const bool use_duplicate_frame = (scene.toolsettings->gpencil_flags & GP_TOOL_FLAG_RETAIN_LAST); + const int target_layer_index = *grease_pencil.get_layer_index(target_layer); + + VectorSet target_frames; + /* Add drawing on the current frame. */ + target_frames.add(scene.r.cfra); + /* Multi-frame edit: Add drawing on frames that are selected in any layer. */ + if (use_multi_frame_editing) { + for (const Layer *layer : grease_pencil.layers()) { + for (const auto [frame_number, frame] : layer->frames().items()) { + if (frame.is_selected()) { + target_frames.add(frame_number); + } + } + } + } + + /* Create new drawings when autokey is enabled. */ + if (use_autokey) { + for (const int frame_number : target_frames) { + if (!target_layer.frames().contains(frame_number)) { + if (use_duplicate_frame) { + grease_pencil.insert_duplicate_frame( + target_layer, *target_layer.frame_key_at(frame_number), frame_number, false); + } + else { + grease_pencil.insert_frame(target_layer, frame_number); + } + } + } + } + + Vector drawings; + for (const int frame_number : target_frames) { + if (Drawing *target_drawing = grease_pencil.get_editable_drawing_at(target_layer, + frame_number)) + { + MutableDrawingInfo target = {*target_drawing, target_layer_index, frame_number, 1.0f}; + + Vector sources; + for (const Layer *source_layer : grease_pencil.layers()) { + if (const Drawing *source_drawing = grease_pencil.get_drawing_at(*source_layer, + frame_number)) + { + const int source_layer_index = *grease_pencil.get_layer_index(*source_layer); + sources.append({*source_drawing, source_layer_index, frame_number, 0}); + } + } + + drawings.append({std::move(target), std::move(sources)}); + } + } + + return drawings; +} + +static bool grease_pencil_apply_fill(bContext &C, wmOperator &op, const wmEvent &event) +{ + using bke::greasepencil::Layer; + using ed::greasepencil::DrawingInfo; + using ed::greasepencil::MutableDrawingInfo; + + constexpr const ed::greasepencil::FillToolFitMethod fit_method = + ed::greasepencil::FillToolFitMethod::FitToView; + /* Debug setting: keep image data blocks for inspection. */ + constexpr const bool keep_images = false; + + ARegion ®ion = *CTX_wm_region(&C); + /* Perform bounds check. */ + const bool in_bounds = BLI_rcti_isect_pt_v(®ion.winrct, event.xy); + if (!in_bounds) { + return false; + } + + wmWindow &win = *CTX_wm_window(&C); + const ViewContext view_context = ED_view3d_viewcontext_init(&C, CTX_data_depsgraph_pointer(&C)); + const Scene &scene = *CTX_data_scene(&C); + Object &object = *CTX_data_active_object(&C); + GreasePencil &grease_pencil = *static_cast(object.data); + auto &op_data = *static_cast(op.customdata); + const ToolSettings &ts = *CTX_data_tool_settings(&C); + const Brush &brush = *BKE_paint_brush(&ts.gp_paint->paint); + const float2 mouse_position = float2(event.mval); + + if (!grease_pencil.has_active_layer()) { + return false; + } + /* Add drawings in the active layer if autokey is enabled. */ + Vector target_drawings = ensure_editable_drawings( + scene, grease_pencil, *grease_pencil.get_active_layer()); + + const VArray boundary_layers = get_fill_boundary_layers( + grease_pencil, eGP_FillLayerModes(brush.gpencil_settings->fill_layer_mode)); + + for (const FillToolTargetInfo &info : target_drawings) { + const Layer &layer = *grease_pencil.layers()[info.target.layer_index]; + + bke::CurvesGeometry fill_curves = fill_strokes(view_context, + brush, + scene, + layer, + boundary_layers, + info.sources, + op_data.invert, + mouse_position, + fit_method, + op_data.material_index, + keep_images); + + Curves *dst_curves_id = curves_new_nomain(std::move(info.target.drawing.strokes_for_write())); + Curves *fill_curves_id = curves_new_nomain(fill_curves); + Array geometry_sets = {bke::GeometrySet::from_curves(dst_curves_id), + bke::GeometrySet::from_curves(fill_curves_id)}; + bke::GeometrySet joined_geometry_set = geometry::join_geometries(geometry_sets, {}); + bke::CurvesGeometry joined_curves = + (joined_geometry_set.has_curves() ? + std::move(joined_geometry_set.get_curves_for_write()->geometry.wrap()) : + bke::CurvesGeometry()); + info.target.drawing.strokes_for_write() = std::move(joined_curves); + info.target.drawing.tag_topology_changed(); + } + + WM_cursor_modal_restore(&win); + + /* Save extend value for next operation. */ + brush.gpencil_settings->fill_extend_fac = op_data.fill_extend_fac; + + return true; +} + +static bool grease_pencil_fill_init(bContext &C, wmOperator &op) +{ + using blender::bke::greasepencil::Layer; + + Main &bmain = *CTX_data_main(&C); + Scene &scene = *CTX_data_scene(&C); + Object &ob = *CTX_data_active_object(&C); + GreasePencil &grease_pencil = *static_cast(ob.data); + Paint &paint = scene.toolsettings->gp_paint->paint; + Brush &brush = *BKE_paint_brush(&paint); + + Layer *layer = grease_pencil.get_active_layer(); + /* Cannot paint in locked layer. */ + if (layer && layer->is_locked()) { + return false; + } + if (layer == nullptr) { + layer = &grease_pencil.add_layer("GP_Layer"); + } + + if (brush.gpencil_settings == nullptr) { + BKE_brush_init_gpencil_settings(&brush); + } + BKE_curvemapping_init(brush.gpencil_settings->curve_sensitivity); + BKE_curvemapping_init(brush.gpencil_settings->curve_strength); + BKE_curvemapping_init(brush.gpencil_settings->curve_jitter); + BKE_curvemapping_init(brush.gpencil_settings->curve_rand_pressure); + BKE_curvemapping_init(brush.gpencil_settings->curve_rand_strength); + BKE_curvemapping_init(brush.gpencil_settings->curve_rand_uv); + BKE_curvemapping_init(brush.gpencil_settings->curve_rand_hue); + BKE_curvemapping_init(brush.gpencil_settings->curve_rand_saturation); + BKE_curvemapping_init(brush.gpencil_settings->curve_rand_value); + + Material *material = BKE_grease_pencil_object_material_ensure_from_active_input_brush( + &bmain, &ob, &brush); + const int material_index = BKE_object_material_index_get(&ob, material); + + op.customdata = MEM_new( + __func__, GreasePencilFillOpData::from_context(C, *layer, material_index)); + return true; +} + +static void grease_pencil_fill_exit(bContext &C, wmOperator &op) +{ + Object &ob = *CTX_data_active_object(&C); + GreasePencil &grease_pencil = *static_cast(ob.data); + + WM_cursor_modal_restore(CTX_wm_window(&C)); + + if (op.customdata) { + MEM_delete(static_cast(op.customdata)); + op.customdata = nullptr; + } + + /* Clear status message area. */ + ED_workspace_status_text(&C, nullptr); + + DEG_id_tag_update(&grease_pencil.id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY); + + WM_main_add_notifier(NC_GEOM | ND_DATA, nullptr); + WM_event_add_notifier(&C, NC_GPENCIL | ND_DATA | NA_EDITED, nullptr); +} + +static int grease_pencil_fill_invoke(bContext *C, wmOperator *op, const wmEvent * /*event*/) +{ + ToolSettings &ts = *CTX_data_tool_settings(C); + Brush &brush = *BKE_paint_brush(&ts.gp_paint->paint); + Object &ob = *CTX_data_active_object(C); + GreasePencil &grease_pencil = *static_cast(ob.data); + + /* Fill tool needs a material (cannot use default material). */ + if ((brush.gpencil_settings->flag & GP_BRUSH_MATERIAL_PINNED) && + brush.gpencil_settings->material == nullptr) + { + BKE_report(op->reports, RPT_ERROR, "Fill tool needs active material"); + return OPERATOR_CANCELLED; + } + if (BKE_object_material_get(&ob, ob.actcol) == nullptr) { + BKE_report(op->reports, RPT_ERROR, "Fill tool needs active material"); + return OPERATOR_CANCELLED; + } + if (!grease_pencil_fill_init(*C, *op)) { + grease_pencil_fill_exit(*C, *op); + return OPERATOR_CANCELLED; + } + const auto &op_data = *static_cast(op->customdata); + + WM_cursor_modal_set(CTX_wm_window(C), WM_CURSOR_PAINT_BRUSH); + grease_pencil_fill_status_indicators(*C, op_data); + + DEG_id_tag_update(&grease_pencil.id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, nullptr); + + /* Add a modal handler for this operator. */ + WM_event_add_modal_handler(C, op); + + return OPERATOR_RUNNING_MODAL; +} + +enum class FillToolModalKey : int8_t { + Cancel = 1, + Confirm, + GapClosureMode, + ExtensionsLengthen, + ExtensionsShorten, + ExtensionsDrag, + ExtensionsCollide, + Invert, + Precision, +}; + +static int grease_pencil_fill_event_modal_map(bContext *C, wmOperator *op, const wmEvent *event) +{ + auto &op_data = *static_cast(op->customdata); + const bool show_extend = (op_data.flag & GP_BRUSH_FILL_SHOW_EXTENDLINES); + // const bool help_lines = (((op_data.flag & GP_BRUSH_FILL_SHOW_HELPLINES) || show_extend) && + // !is_inverted); + // const bool extend_lines = (op_data.fill_extend_fac > 0.0f); + const float extension_delta = (op_data.precision ? 0.01f : 0.1f); + + switch (event->val) { + case int(FillToolModalKey::Cancel): + return OPERATOR_CANCELLED; + + case int(FillToolModalKey::Confirm): { + /* Ignore in extension mode. */ + if (op_data.is_extension_mode) { + break; + } + + op_data.fill_mouse_pos = float2(event->mval); + return (grease_pencil_apply_fill(*C, *op, *event) ? OPERATOR_FINISHED : OPERATOR_CANCELLED); + } + + case int(FillToolModalKey::GapClosureMode): + if (show_extend && event->val == KM_PRESS) { + /* Toggle mode. */ + if (op_data.fill_extend_mode == GP_FILL_EMODE_EXTEND) { + op_data.fill_extend_mode = GP_FILL_EMODE_RADIUS; + } + else { + op_data.fill_extend_mode = GP_FILL_EMODE_EXTEND; + } + grease_pencil_update_extend(*C, op_data); + } + break; + + case int(FillToolModalKey::ExtensionsLengthen): + op_data.fill_extend_fac = std::max(op_data.fill_extend_fac - extension_delta, 0.0f); + grease_pencil_update_extend(*C, op_data); + break; + + case int(FillToolModalKey::ExtensionsShorten): + op_data.fill_extend_fac = std::min(op_data.fill_extend_fac + extension_delta, 10.0f); + grease_pencil_update_extend(*C, op_data); + break; + + case int(FillToolModalKey::ExtensionsDrag): { + if (event->val == KM_PRESS) { + /* Consider initial offset as zero position. */ + op_data.is_extension_mode = true; + /* TODO This is the GPv2 logic and it's weird. Should be reconsidered, for now use the + * same method. */ + const float2 base_pos = float2(event->mval); + constexpr const float gap = 300.0f; + op_data.extension_mouse_pos = (math::distance(base_pos, op_data.fill_mouse_pos) >= gap ? + base_pos : + base_pos - float2(gap, 0)); + WM_cursor_set(CTX_wm_window(C), WM_CURSOR_EW_ARROW); + } + if (event->val == KM_RELEASE) { + WM_cursor_modal_set(CTX_wm_window(C), WM_CURSOR_PAINT_BRUSH); + op_data.is_extension_mode = false; + } + /* Update cursor line. */ + WM_main_add_notifier(NC_GEOM | ND_DATA, nullptr); + WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, nullptr); + break; + } + + case int(FillToolModalKey::ExtensionsCollide): + if (show_extend && event->val == KM_PRESS) { + op_data.flag ^= GP_BRUSH_FILL_STROKE_COLLIDE; + grease_pencil_update_extend(*C, op_data); + } + break; + + case int(FillToolModalKey::Invert): + op_data.invert = !op_data.invert; + break; + + case int(FillToolModalKey::Precision): + op_data.precision = !op_data.precision; + break; + + default: + BLI_assert_unreachable(); + break; + } + return OPERATOR_RUNNING_MODAL; +} + +static int grease_pencil_fill_modal(bContext *C, wmOperator *op, const wmEvent *event) +{ + const RegionView3D &rv3d = *CTX_wm_region_view3d(C); + + auto &op_data = *static_cast(op->customdata); + + int estate = OPERATOR_RUNNING_MODAL; + switch (event->type) { + case EVT_MODAL_MAP: + estate = grease_pencil_fill_event_modal_map(C, op, event); + break; + case MOUSEMOVE: { + if (!op_data.is_extension_mode) { + break; + } + + const Object &ob = *CTX_data_active_object(C); + const float pixel_size = ED_view3d_pixel_size(&rv3d, ob.loc); + const float2 mouse_pos = float2(event->mval); + const float initial_dist = math::distance(op_data.extension_mouse_pos, + op_data.fill_mouse_pos); + const float current_dist = math::distance(mouse_pos, op_data.fill_mouse_pos); + + float delta = (current_dist - initial_dist) * pixel_size * 0.5f; + op_data.fill_extend_fac = std::clamp(op_data.fill_extend_fac + delta, 0.0f, 10.0f); + + /* Update cursor line and extend lines. */ + WM_main_add_notifier(NC_GEOM | ND_DATA, nullptr); + WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, nullptr); + + grease_pencil_update_extend(*C, op_data); + break; + } + default: + break; + } + /* Process last operations before exiting. */ + switch (estate) { + case OPERATOR_FINISHED: + grease_pencil_fill_exit(*C, *op); + WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, nullptr); + break; + + case OPERATOR_CANCELLED: + grease_pencil_fill_exit(*C, *op); + break; + + default: + break; + } + + return estate; +} + +static void grease_pencil_fill_cancel(bContext *C, wmOperator *op) +{ + grease_pencil_fill_exit(*C, *op); +} + +static void GREASE_PENCIL_OT_fill(wmOperatorType *ot) +{ + PropertyRNA *prop; + + ot->name = "Grease Pencil Fill"; + ot->idname = "GREASE_PENCIL_OT_fill"; + ot->description = "Fill with color the shape formed by strokes"; + + ot->poll = ed::greasepencil::grease_pencil_painting_poll; + ot->invoke = grease_pencil_fill_invoke; + ot->modal = grease_pencil_fill_modal; + ot->cancel = grease_pencil_fill_cancel; + + ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER; + + prop = RNA_def_boolean(ot->srna, "on_back", false, "Draw on Back", "Send new stroke to back"); + RNA_def_property_flag(prop, PROP_SKIP_SAVE); +} + +/** \} */ + } // namespace blender::ed::sculpt_paint /* -------------------------------------------------------------------- */ @@ -476,6 +1060,49 @@ void ED_operatortypes_grease_pencil_draw() WM_operatortype_append(GREASE_PENCIL_OT_brush_stroke); WM_operatortype_append(GREASE_PENCIL_OT_sculpt_paint); WM_operatortype_append(GREASE_PENCIL_OT_weight_brush_stroke); + WM_operatortype_append(GREASE_PENCIL_OT_fill); +} + +void ED_filltool_modal_keymap(wmKeyConfig *keyconf) +{ + using namespace blender::ed::greasepencil; + using blender::ed::sculpt_paint::FillToolModalKey; + + static const EnumPropertyItem modal_items[] = { + {int(FillToolModalKey::Cancel), "CANCEL", 0, "Cancel", ""}, + {int(FillToolModalKey::Confirm), "CONFIRM", 0, "Confirm", ""}, + {int(FillToolModalKey::GapClosureMode), "GAP_CLOSURE_MODE", 0, "Gap Closure Mode", ""}, + {int(FillToolModalKey::ExtensionsLengthen), + "EXTENSIONS_LENGTHEN", + 0, + "Length Extensions", + ""}, + {int(FillToolModalKey::ExtensionsShorten), + "EXTENSIONS_SHORTEN", + 0, + "Shorten Extensions", + ""}, + {int(FillToolModalKey::ExtensionsDrag), "EXTENSIONS_DRAG", 0, "Drag Extensions", ""}, + {int(FillToolModalKey::ExtensionsCollide), + "EXTENSIONS_COLLIDE", + 0, + "Collide Extensions", + ""}, + {int(FillToolModalKey::Invert), "INVERT", 0, "Invert", ""}, + {int(FillToolModalKey::Precision), "PRECISION", 0, "Precision", ""}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + wmKeyMap *keymap = WM_modalkeymap_find(keyconf, "Fill Tool Modal Map"); + + /* This function is called for each space-type, only needs to add map once. */ + if (keymap && keymap->modal_items) { + return; + } + + keymap = WM_modalkeymap_ensure(keyconf, "Fill Tool Modal Map", modal_items); + + WM_modalkeymap_assign(keymap, "GREASE_PENCIL_OT_fill"); } /** \} */ diff --git a/source/blender/editors/sculpt_paint/grease_pencil_fill.cc b/source/blender/editors/sculpt_paint/grease_pencil_fill.cc new file mode 100644 index 00000000000..85211e1e3d3 --- /dev/null +++ b/source/blender/editors/sculpt_paint/grease_pencil_fill.cc @@ -0,0 +1,1041 @@ +/* SPDX-FileCopyrightText: 2024 Blender Authors + * + * SPDX-License-Identifier: GPL-2.0-or-later */ + +#include "BLI_color.hh" +#include "BLI_index_mask.hh" +#include "BLI_math_base.hh" +#include "BLI_math_matrix.hh" +#include "BLI_math_vector.hh" +#include "BLI_offset_indices.hh" +#include "BLI_rect.h" +#include "BLI_stack.hh" +#include "BLI_task.hh" + +#include "BKE_attribute.hh" +#include "BKE_camera.h" +#include "BKE_context.hh" +#include "BKE_crazyspace.hh" +#include "BKE_curves.hh" +#include "BKE_grease_pencil.hh" +#include "BKE_image.h" +#include "BKE_lib_id.hh" +#include "BKE_material.h" +#include "BKE_paint.hh" + +#include "DNA_curves_types.h" +#include "DNA_grease_pencil_types.h" +#include "DNA_material_types.h" +#include "DNA_object_types.h" +#include "DNA_scene_types.h" +#include "DNA_view3d_types.h" + +#include "DEG_depsgraph_query.hh" + +#include "ED_grease_pencil.hh" +#include "ED_view3d.hh" + +#include "IMB_imbuf.hh" +#include "IMB_imbuf_types.hh" + +#include "GPU_state.hh" + +#include +#include + +namespace blender::ed::greasepencil { + +/* -------------------------------------------------------------------- */ +/** \name Color Values and Flags + * \{ */ + +const ColorGeometry4f draw_boundary_color = {1, 0, 0, 1}; +const ColorGeometry4f draw_seed_color = {0, 1, 0, 1}; + +enum ColorFlag { + Border = (1 << 0), + Stroke = (1 << 1), + Fill = (1 << 2), + Seed = (1 << 3), + Debug = (1 << 7), +}; +ENUM_OPERATORS(ColorFlag, ColorFlag::Seed) + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Boundary from Pixel Buffer + * \{ */ + +/* Utility class for access to pixel buffer data. */ +class ImageBufferAccessor { + private: + Image *ima_ = nullptr; + ImBuf *ibuf_ = nullptr; + void *lock_ = nullptr; + MutableSpan data_; + int2 size_ = int2(0); + + public: + bool has_buffer() const + { + return ibuf_ != nullptr; + } + + ~ImageBufferAccessor() + { + BLI_assert(!this->has_buffer()); + } + + void acquire(Image &ima) + { + BLI_assert(!this->has_buffer()); + ima_ = &ima; + ibuf_ = BKE_image_acquire_ibuf(&ima, nullptr, &lock_); + size_ = {ibuf_->x, ibuf_->y}; + data_ = MutableSpan( + reinterpret_cast(ibuf_->byte_buffer.data), ibuf_->x * ibuf_->y); + } + + void release() + { + BLI_assert(this->has_buffer()); + BKE_image_release_ibuf(ima_, ibuf_, lock_); + lock_ = nullptr; + ima_ = nullptr; + ibuf_ = nullptr; + data_ = {}; + size_ = int2(0); + } + + int2 size() const + { + return this->size_; + } + + int width() const + { + return this->size_.x; + } + + int height() const + { + return this->size_.y; + } + + bool is_valid_coord(const int2 &c) const + { + return c.x >= 0 && c.x < this->size_.x && c.y >= 0 && c.y < this->size_.y; + } + + int2 coord_from_index(const int index) const + { + const div_t d = div(index, this->size_.x); + return int2{d.rem, d.quot}; + } + + int index_from_coord(const int2 &c) const + { + return c.x + c.y * this->size_.x; + } + + Span pixels() const + { + return this->data_; + } + + MutableSpan pixels() + { + return this->data_; + } + + ColorGeometry4b &pixel_from_coord(const int2 &c) + { + return this->data_[index_from_coord(c)]; + } + + const ColorGeometry4b &pixel_from_coord(const int2 &c) const + { + return this->data_[index_from_coord(c)]; + } +}; + +static bool get_flag(const ColorGeometry4b &color, const ColorFlag flag) +{ + return (color.r & flag) != 0; +} + +static void set_flag(ColorGeometry4b &color, const ColorFlag flag, bool value) +{ + color.r = value ? (color.r | flag) : (color.r & (~flag)); +} + +/* Set a border to create image limits. */ +/* TODO this shouldn't be necessary if drawing could accurately save flag values. */ +static void convert_colors_to_flags(ImageBufferAccessor &buffer) +{ + for (ColorGeometry4b &color : buffer.pixels()) { + const bool is_stroke = color.r > 0.0f; + const bool is_seed = color.g > 0.0f; + color.r = (is_stroke ? ColorFlag::Stroke : 0) | (is_seed ? ColorFlag::Seed : 0); + color.g = 0; + color.b = 0; + color.a = 0; + } +} + +/* Set a border to create image limits. */ +static void convert_flags_to_colors(ImageBufferAccessor &buffer) +{ + constexpr const ColorGeometry4b output_stroke_color = {255, 0, 0, 255}; + constexpr const ColorGeometry4b output_seed_color = {127, 127, 0, 255}; + constexpr const ColorGeometry4b output_border_color = {0, 0, 255, 255}; + constexpr const ColorGeometry4b output_fill_color = {127, 255, 0, 255}; + // constexpr const ColorGeometry4b output_extend_color = {25, 255, 0, 255}; + // constexpr const ColorGeometry4b output_helper_color = {255, 0, 127, 255}; + constexpr const ColorGeometry4b output_debug_color = {255, 127, 0, 255}; + + auto add_colors = [](const ColorGeometry4b &a, const ColorGeometry4b &b) -> ColorGeometry4b { + return ColorGeometry4b(std::min(int(a.r) + int(b.r), 255), + std::min(int(a.g) + int(b.g), 255), + std::min(int(a.b) + int(b.b), 255), + std::min(int(a.a) + int(b.a), 255)); + }; + + for (ColorGeometry4b &color : buffer.pixels()) { + ColorGeometry4b output_color = ColorGeometry4b(0, 0, 0, 0); + if (color.r & ColorFlag::Debug) { + output_color = add_colors(output_color, output_debug_color); + } + if (color.r & ColorFlag::Fill) { + output_color = add_colors(output_color, output_fill_color); + } + if (color.r & ColorFlag::Stroke) { + output_color = add_colors(output_color, output_stroke_color); + } + if (color.r & ColorFlag::Border) { + output_color = add_colors(output_color, output_border_color); + } + if (color.r & ColorFlag::Seed) { + output_color = add_colors(output_color, output_seed_color); + } + color = std::move(output_color); + } +} + +/* Set a border to create image limits. */ +static void mark_borders(ImageBufferAccessor &buffer) +{ + int row_start = 0; + /* Fill first row */ + for (const int i : IndexRange(buffer.width())) { + set_flag(buffer.pixels()[row_start + i], ColorFlag::Border, true); + } + row_start += buffer.width(); + /* Fill first and last pixel of middle rows. */ + for ([[maybe_unused]] const int i : IndexRange(buffer.height()).drop_front(1).drop_back(1)) { + set_flag(buffer.pixels()[row_start], ColorFlag::Border, true); + set_flag(buffer.pixels()[row_start + buffer.width() - 1], ColorFlag::Border, true); + row_start += buffer.width(); + } + /* Fill last row */ + for (const int i : IndexRange(buffer.width())) { + set_flag(buffer.pixels()[row_start + i], ColorFlag::Border, true); + } +} + +enum class FillResult { + Success, + BorderContact, +}; + +enum FillBorderMode { + /* Cancel when hitting the border, fill failed. */ + Cancel, + /* Allow border contact, continue with other pixels. */ + Ignore, +}; + +template +FillResult flood_fill(ImageBufferAccessor &buffer, const int leak_filter_width = 0) +{ + const MutableSpan pixels = buffer.pixels(); + const int width = buffer.width(); + const int height = buffer.height(); + + blender::Stack active_pixels; + /* Initialize the stack with filled pixels (dot at mouse position). */ + for (const int i : pixels.index_range()) { + if (get_flag(pixels[i], ColorFlag::Seed)) { + active_pixels.push(i); + } + } + + enum FilterDirection { + Horizontal = 1, + Vertical = 2, + }; + + bool border_contact = false; + while (!active_pixels.is_empty()) { + const int index = active_pixels.pop(); + const int2 coord = buffer.coord_from_index(index); + ColorGeometry4b pixel_value = buffer.pixels()[index]; + + if constexpr (border_mode == FillBorderMode::Cancel) { + if (get_flag(pixel_value, ColorFlag::Border)) { + border_contact = true; + break; + } + } + else if constexpr (border_mode == FillBorderMode::Ignore) { + if (get_flag(pixel_value, ColorFlag::Border)) { + border_contact = true; + } + } + + if (get_flag(pixel_value, ColorFlag::Fill)) { + /* Pixel already filled. */ + continue; + } + + if (get_flag(pixel_value, ColorFlag::Stroke)) { + /* Boundary pixel, ignore. */ + continue; + } + + /* Mark as filled. */ + set_flag(pixels[index], ColorFlag::Fill, true); + + /* Directional box filtering for gap detection. */ + const IndexRange filter_x_neg = IndexRange(1, std::min(coord.x, leak_filter_width)); + const IndexRange filter_x_pos = IndexRange(1, + std::min(width - 1 - coord.x, leak_filter_width)); + const IndexRange filter_y_neg = IndexRange(1, std::min(coord.y, leak_filter_width)); + const IndexRange filter_y_pos = IndexRange(1, + std::min(height - 1 - coord.y, leak_filter_width)); + bool is_boundary_horizontal = false; + bool is_boundary_vertical = false; + for (const int filter_i : filter_y_neg) { + is_boundary_horizontal |= get_flag(buffer.pixel_from_coord(coord - int2(0, filter_i)), + ColorFlag::Stroke); + } + for (const int filter_i : filter_y_pos) { + is_boundary_horizontal |= get_flag(buffer.pixel_from_coord(coord + int2(0, filter_i)), + ColorFlag::Stroke); + } + for (const int filter_i : filter_x_neg) { + is_boundary_vertical |= get_flag(buffer.pixel_from_coord(coord - int2(filter_i, 0)), + ColorFlag::Stroke); + } + for (const int filter_i : filter_x_pos) { + is_boundary_vertical |= get_flag(buffer.pixel_from_coord(coord + int2(filter_i, 0)), + ColorFlag::Stroke); + } + + /* Activate neighbors */ + if (coord.x > 0 && !is_boundary_horizontal) { + active_pixels.push(buffer.index_from_coord(coord - int2{1, 0})); + } + if (coord.x < width - 1 && !is_boundary_horizontal) { + active_pixels.push(buffer.index_from_coord(coord + int2{1, 0})); + } + if (coord.y > 0 && !is_boundary_vertical) { + active_pixels.push(buffer.index_from_coord(coord - int2{0, 1})); + } + if (coord.y < height - 1 && !is_boundary_vertical) { + active_pixels.push(buffer.index_from_coord(coord + int2{0, 1})); + } + } + + return border_contact ? FillResult::BorderContact : FillResult::Success; +} + +/* Turn unfilled areas into filled and vice versa. */ +static void invert_fill(ImageBufferAccessor &buffer) +{ + for (ColorGeometry4b &color : buffer.pixels()) { + const bool is_filled = get_flag(color, ColorFlag::Fill); + set_flag(color, ColorFlag::Fill, !is_filled); + } +} + +constexpr const int num_directions = 8; +static const int2 offset_by_direction[num_directions] = { + {-1, -1}, + {0, -1}, + {1, -1}, + {1, 0}, + {1, 1}, + {0, 1}, + {-1, 1}, + {-1, 0}, +}; + +/* Wrap to valid direction, must be less than 3 * num_directions. */ +static int wrap_dir_3n(const int dir) +{ + return dir - num_directions * (int(dir >= num_directions) + int(dir >= 2 * num_directions)); +} + +struct FillBoundary { + /* Pixel indices making up boundary curves. */ + Vector pixels; + /* Offset index for each curve. */ + Vector offset_indices; +}; + +/* Get the outline points of a shape using Moore Neighborhood algorithm + * + * This is a Blender customized version of the general algorithm described + * in https://en.wikipedia.org/wiki/Moore_neighborhood + */ +static FillBoundary build_fill_boundary(const ImageBufferAccessor &buffer) +{ + using BoundarySection = std::list; + using BoundaryStartMap = Map; + + const Span pixels = buffer.pixels(); + const int width = buffer.width(); + const int height = buffer.height(); + + /* Find possible starting points for boundary sections. + * Direction 3 == (1, 0) is the starting direction. */ + constexpr const uint8_t start_direction = 3; + auto find_start_coordinates = [&]() -> BoundaryStartMap { + BoundaryStartMap starts; + for (const int y : IndexRange(height)) { + /* Check for empty pixels next to filled pixels. */ + for (const int x : IndexRange(width).drop_back(1)) { + const int index_left = buffer.index_from_coord({x, y}); + const int index_right = buffer.index_from_coord({x + 1, y}); + const bool filled_left = get_flag(pixels[index_left], ColorFlag::Fill); + const bool filled_right = get_flag(pixels[index_right], ColorFlag::Fill); + const bool border_right = get_flag(pixels[index_right], ColorFlag::Border); + if (!filled_left && filled_right && !border_right) { + /* Empty index list indicates uninitialized section. */ + starts.add(index_right, {}); + } + } + } + return starts; + }; + + struct NeighborIterator { + int index; + int direction; + }; + + /* Find the next filled pixel in clockwise direction from the current. */ + auto find_next_neighbor = [&](NeighborIterator &iter) -> bool { + const int2 iter_coord = buffer.coord_from_index(iter.index); + for (const int i : IndexRange(num_directions).drop_front(1)) { + /* Invert direction (add 4) and start at next direction (add 1..n). + * This can not be greater than 3*num_directions-1, wrap accordingly. */ + const int neighbor_dir = wrap_dir_3n(iter.direction + 4 + i); + const int2 neighbor_coord = iter_coord + offset_by_direction[neighbor_dir]; + if (!buffer.is_valid_coord(neighbor_coord)) { + continue; + } + const int neighbor_index = buffer.index_from_coord(neighbor_coord); + /* Border pixels are not valid. */ + if (get_flag(pixels[neighbor_index], ColorFlag::Border)) { + continue; + } + if (get_flag(pixels[neighbor_index], ColorFlag::Fill)) { + iter.index = neighbor_index; + iter.direction = neighbor_dir; + return true; + } + } + return false; + }; + + BoundaryStartMap boundary_starts = find_start_coordinates(); + + /* Find directions and connectivity for all boundary pixels. */ + for (const int start_index : boundary_starts.keys()) { + /* Boundary map entries may get removed, only handle active starts. */ + if (!boundary_starts.contains(start_index)) { + continue; + } + BoundarySection §ion = boundary_starts.lookup(start_index); + section.push_back(start_index); + NeighborIterator iter = {start_index, start_direction}; + while (find_next_neighbor(iter)) { + /* Loop closed when arriving at start again. */ + if (iter.index == start_index) { + break; + } + + /* Join existing sections. */ + if (boundary_starts.contains(iter.index)) { + BoundarySection &next_section = boundary_starts.lookup(iter.index); + if (next_section.empty()) { + /* Empty sections are only start indices, remove and continue. */ + boundary_starts.remove(iter.index); + } + else { + /* Merge existing points into the current section. */ + section.splice(section.end(), next_section); + boundary_starts.remove(iter.index); + break; + } + } + + section.push_back(iter.index); + } + /* Discard unclosed boundaries. */ + if (iter.index != start_index) { + boundary_starts.remove(start_index); + } + } + + /* Construct final strokes by tracing the boundary. */ + FillBoundary final_boundary; + for (const BoundarySection §ion : boundary_starts.values()) { + final_boundary.offset_indices.append(final_boundary.pixels.size()); + for (const int index : section) { + final_boundary.pixels.append(index); + } + } + final_boundary.offset_indices.append(final_boundary.pixels.size()); + + return final_boundary; +} + +/* Create curves geometry from boundary positions. */ +static bke::CurvesGeometry boundary_to_curves(const Scene &scene, + const ViewContext &view_context, + const Brush &brush, + const FillBoundary &boundary, + const ImageBufferAccessor &buffer, + const ed::greasepencil::DrawingPlacement &placement, + const int material_index, + const float hardness) +{ + /* Curve cannot have 0 points. */ + if (boundary.offset_indices.is_empty() || boundary.pixels.is_empty()) { + return {}; + } + + bke::CurvesGeometry curves(boundary.pixels.size(), boundary.offset_indices.size() - 1); + + curves.offsets_for_write().copy_from(boundary.offset_indices); + MutableSpan positions = curves.positions_for_write(); + bke::MutableAttributeAccessor attributes = curves.attributes_for_write(); + /* Attributes that are defined explicitly and should not be set to default values. */ + Set skip_curve_attributes = {"curve_type", "material_index", "cyclic", "hardness"}; + Set skip_point_attributes = {"position", "radius", "opacity"}; + + curves.curve_types_for_write().fill(CURVE_TYPE_POLY); + curves.update_curve_types(); + + bke::SpanAttributeWriter materials = attributes.lookup_or_add_for_write_span( + "material_index", bke::AttrDomain::Curve); + bke::SpanAttributeWriter cyclic = attributes.lookup_or_add_for_write_span( + "cyclic", bke::AttrDomain::Curve); + bke::SpanAttributeWriter hardnesses = attributes.lookup_or_add_for_write_span( + "hardness", + bke::AttrDomain::Curve, + bke::AttributeInitVArray(VArray::ForSingle(1.0f, curves.curves_num()))); + bke::SpanAttributeWriter radii = attributes.lookup_or_add_for_write_span( + "radius", + bke::AttrDomain::Point, + bke::AttributeInitVArray(VArray::ForSingle(0.01f, curves.points_num()))); + bke::SpanAttributeWriter opacities = attributes.lookup_or_add_for_write_span( + "opacity", + bke::AttrDomain::Point, + bke::AttributeInitVArray(VArray::ForSingle(1.0f, curves.points_num()))); + + cyclic.span.fill(true); + materials.span.fill(material_index); + hardnesses.span.fill(hardness); + + cyclic.finish(); + materials.finish(); + hardnesses.finish(); + + for (const int point_i : curves.points_range()) { + const int pixel_index = boundary.pixels[point_i]; + const int2 pixel_coord = buffer.coord_from_index(pixel_index); + const float3 position = placement.project(float2(pixel_coord)); + positions[point_i] = position; + + /* Calculate radius and opacity for the outline as if it was a user stroke with full pressure. + */ + constexpr const float pressure = 1.0f; + radii.span[point_i] = ed::greasepencil::radius_from_input_sample( + pressure, position, view_context, &brush, &scene, brush.gpencil_settings); + opacities.span[point_i] = ed::greasepencil::opacity_from_input_sample( + pressure, &brush, &scene, brush.gpencil_settings); + } + + if (scene.toolsettings->gp_paint->mode == GPPAINT_FLAG_USE_VERTEXCOLOR) { + ColorGeometry4f vertex_color; + srgb_to_linearrgb_v3_v3(vertex_color, brush.rgb); + vertex_color.a = brush.gpencil_settings->vertex_factor; + + if (ELEM(brush.gpencil_settings->vertex_mode, GPPAINT_MODE_FILL, GPPAINT_MODE_BOTH)) { + skip_curve_attributes.add("fill_color"); + bke::SpanAttributeWriter fill_colors = + attributes.lookup_or_add_for_write_span("fill_color", + bke::AttrDomain::Curve); + fill_colors.span.fill(vertex_color); + fill_colors.finish(); + } + if (ELEM(brush.gpencil_settings->vertex_mode, GPPAINT_MODE_STROKE, GPPAINT_MODE_BOTH)) { + skip_point_attributes.add("vertex_color"); + bke::SpanAttributeWriter vertex_colors = + attributes.lookup_or_add_for_write_span("vertex_color", + bke::AttrDomain::Point); + vertex_colors.span.fill(vertex_color); + vertex_colors.finish(); + } + } + + radii.finish(); + opacities.finish(); + + /* Initialize the rest of the attributes with default values. */ + bke::fill_attribute_range_default( + attributes, bke::AttrDomain::Curve, skip_curve_attributes, curves.curves_range()); + bke::fill_attribute_range_default( + attributes, bke::AttrDomain::Point, skip_point_attributes, curves.points_range()); + + return curves; +} + +static bke::CurvesGeometry process_image(Image &ima, + const Scene &scene, + const ViewContext &view_context, + const Brush &brush, + const ed::greasepencil::DrawingPlacement &placement, + const int stroke_material_index, + const float stroke_hardness, + const bool invert, + const bool output_as_colors) +{ + constexpr const int leak_filter_width = 3; + + ImageBufferAccessor buffer; + buffer.acquire(ima); + BLI_SCOPED_DEFER([&]() { + if (output_as_colors) { + /* For visual output convert bit flags back to colors. */ + convert_flags_to_colors(buffer); + } + buffer.release(); + }); + + convert_colors_to_flags(buffer); + + /* Set red borders to create a external limit. */ + mark_borders(buffer); + + /* Apply boundary fill */ + if (invert) { + /* When inverted accept border fill, image borders are valid boundaries. */ + FillResult fill_result = flood_fill(buffer, leak_filter_width); + if (!ELEM(fill_result, FillResult::Success, FillResult::BorderContact)) { + return {}; + } + /* Make fills into boundaries and vice versa for finding exterior boundaries. */ + invert_fill(buffer); + } + else { + /* Cancel when encountering a border, counts as failure. */ + FillResult fill_result = flood_fill(buffer, leak_filter_width); + if (fill_result != FillResult::Success) { + return {}; + } + } + + const FillBoundary boundary = build_fill_boundary(buffer); + + return boundary_to_curves(scene, + view_context, + brush, + boundary, + buffer, + placement, + stroke_material_index, + stroke_hardness); +} + +/** \} */ + +constexpr const char *attr_material_index = "material_index"; +constexpr const char *attr_is_boundary = "is_boundary"; + +static IndexMask get_visible_boundary_strokes(const Object &object, + const DrawingInfo &info, + const bool is_boundary_layer, + IndexMaskMemory &memory) +{ + const bke::CurvesGeometry &strokes = info.drawing.strokes(); + const bke::AttributeAccessor attributes = strokes.attributes(); + const VArray materials = *attributes.lookup(attr_material_index, + bke::AttrDomain::Curve); + + auto is_visible_curve = [&](const int curve_i) { + /* Check if stroke can be drawn. */ + const IndexRange points = strokes.points_by_curve()[curve_i]; + if (points.size() < 2) { + return false; + } + + /* Check if the material is visible. */ + const Material *material = BKE_object_material_get(const_cast(&object), + materials[curve_i] + 1); + const MaterialGPencilStyle *gp_style = material ? material->gp_style : nullptr; + const bool is_hidden_material = (gp_style->flag & GP_MATERIAL_HIDE); + if (gp_style == nullptr || is_hidden_material) { + return false; + } + + return true; + }; + + /* On boundary layers only boundary strokes are rendered. */ + if (is_boundary_layer) { + const VArray boundary_strokes = *attributes.lookup_or_default( + attr_is_boundary, bke::AttrDomain::Curve, false); + + return IndexMask::from_predicate( + strokes.curves_range(), GrainSize(512), memory, [&](const int curve_i) { + if (!is_visible_curve(curve_i)) { + return false; + } + const bool is_boundary_stroke = boundary_strokes[curve_i]; + return is_boundary_stroke; + }); + } + + return IndexMask::from_predicate( + strokes.curves_range(), GrainSize(512), memory, is_visible_curve); +} + +static VArray stroke_colors(const Object &object, + const bke::CurvesGeometry &curves, + const VArray &opacities, + const VArray materials, + const ColorGeometry4f &tint_color, + const float alpha_threshold, + const bool brush_fill_hide) +{ + if (brush_fill_hide) { + return VArray::ForSingle(tint_color, curves.points_num()); + } + + Array colors(curves.points_num()); + threading::parallel_for(curves.curves_range(), 512, [&](const IndexRange range) { + for (const int curve_i : range) { + const Material *material = BKE_object_material_get(const_cast(&object), + materials[curve_i] + 1); + const float material_alpha = material && material->gp_style ? + material->gp_style->stroke_rgba[3] : + 1.0f; + const IndexRange points = curves.points_by_curve()[curve_i]; + for (const int point_i : points) { + const float alpha = (material_alpha * opacities[point_i] > alpha_threshold ? 1.0f : 0.0f); + colors[point_i] = ColorGeometry4f(tint_color.r, tint_color.g, tint_color.b, alpha); + } + } + }); + return VArray::ForContainer(colors); +} + +static rctf get_region_bounds(const ARegion ®ion) +{ + /* Init maximum boundbox size. */ + rctf region_bounds; + BLI_rctf_init(®ion_bounds, 0, region.winx, 0, region.winy); + return region_bounds; +} + +/* Helper: Calc the maximum bounding box size of strokes to get the zoom level of the viewport. + * For each stroke, the 2D projected bounding box is calculated and using this data, the total + * object bounding box (all strokes) is calculated. */ +static rctf get_boundary_bounds(const ARegion ®ion, + const RegionView3D &rv3d, + const Object &object, + const Object &object_eval, + const VArray &boundary_layers, + const Span src_drawings) +{ + using bke::greasepencil::Drawing; + using bke::greasepencil::Layer; + + rctf bounds; + BLI_rctf_init_minmax(&bounds); + + BLI_assert(object.type == OB_GREASE_PENCIL); + GreasePencil &grease_pencil = *static_cast(object.data); + + BLI_assert(grease_pencil.has_active_layer()); + + for (const DrawingInfo &info : src_drawings) { + const Layer &layer = *grease_pencil.layers()[info.layer_index]; + const float4x4 layer_to_world = layer.to_world_space(object); + const bke::crazyspace::GeometryDeformation deformation = + bke::crazyspace::get_evaluated_grease_pencil_drawing_deformation( + &object_eval, object, info.layer_index, info.frame_number); + const bool only_boundary_strokes = boundary_layers[info.layer_index]; + const VArray radii = info.drawing.radii(); + const bke::CurvesGeometry &strokes = info.drawing.strokes(); + const bke::AttributeAccessor attributes = strokes.attributes(); + const VArray materials = *attributes.lookup(attr_material_index, + bke::AttrDomain::Curve); + const VArray is_boundary_stroke = *attributes.lookup_or_default( + "is_boundary", bke::AttrDomain::Curve, false); + + IndexMaskMemory curve_mask_memory; + const IndexMask curve_mask = get_visible_boundary_strokes( + object, info, only_boundary_strokes, curve_mask_memory); + + curve_mask.foreach_index(GrainSize(512), [&](const int curve_i) { + const IndexRange points = strokes.points_by_curve()[curve_i]; + /* Check if stroke can be drawn. */ + if (points.size() < 2) { + return; + } + /* Check if the color is visible. */ + const int material_index = materials[curve_i]; + Material *mat = BKE_object_material_get(const_cast(&object), material_index + 1); + if (mat == 0 || (mat->gp_style->flag & GP_MATERIAL_HIDE)) { + return; + } + + /* In boundary layers only boundary strokes should be rendered. */ + if (only_boundary_strokes && !is_boundary_stroke[curve_i]) { + return; + } + + for (const int point_i : points) { + const float3 pos_world = math::transform_point(layer_to_world, + deformation.positions[point_i]); + float2 pos_view; + eV3DProjStatus result = ED_view3d_project_float_global( + ®ion, pos_world, pos_view, V3D_PROJ_TEST_NOP); + if (result == V3D_PROJ_RET_OK) { + const float pixels = radii[point_i] / ED_view3d_pixel_size(&rv3d, pos_world); + rctf point_rect; + BLI_rctf_init_pt_radius(&point_rect, pos_view, pixels); + BLI_rctf_union(&bounds, &point_rect); + } + } + }); + } + + return bounds; +} + +static auto fit_strokes_to_view(const ViewContext &view_context, + const VArray &boundary_layers, + const Span src_drawings, + const FillToolFitMethod fit_method, + const float2 fill_point, + const bool uniform_zoom, + const float max_zoom_factor, + const float2 margin) +{ + BLI_assert(max_zoom_factor >= 1.0f); + const float min_zoom_factor = math::safe_rcp(max_zoom_factor); + + switch (fit_method) { + case FillToolFitMethod::None: + return std::make_pair(float2(1.0f), float2(0.0f)); + + case FillToolFitMethod::FitToView: { + const Object &object_eval = *DEG_get_evaluated_object(view_context.depsgraph, + view_context.obact); + /* Zoom and offset based on bounds, to fit all strokes within the render. */ + const rctf bounds = get_boundary_bounds(*view_context.region, + *view_context.rv3d, + *view_context.obact, + object_eval, + boundary_layers, + src_drawings); + const rctf region_bounds = get_region_bounds(*view_context.region); + UNUSED_VARS(bounds, region_bounds); + const float2 bounds_max = float2(bounds.xmax, bounds.ymax); + const float2 bounds_min = float2(bounds.xmin, bounds.ymin); + /* Include fill point for computing zoom. */ + const float2 fill_bounds_min = math::min(bounds_min, fill_point) - margin; + const float2 fill_bounds_max = math::max(bounds_max, fill_point) + margin; + const float2 fill_bounds_center = 0.5f * (fill_bounds_min + fill_bounds_max); + const float2 fill_bounds_extent = fill_bounds_max - fill_bounds_min; + + const float2 region_max = float2(region_bounds.xmax, region_bounds.ymax); + const float2 region_min = float2(region_bounds.xmin, region_bounds.ymin); + const float2 region_center = 0.5f * (region_min + region_max); + const float2 region_extent = region_max - region_min; + + const float2 zoom_factors = math::clamp(math::safe_divide(fill_bounds_extent, region_extent), + float2(min_zoom_factor), + float2(max_zoom_factor)); + /* Use the most zoomed out factor for uniform scale. */ + const float2 zoom = uniform_zoom ? float2(math::reduce_max(zoom_factors)) : zoom_factors; + + /* Clamp offset to always include the center point. */ + const float2 offset_center = fill_bounds_center - region_center; + const float2 offset_min = fill_point + 0.5f * fill_bounds_extent - region_center; + const float2 offset_max = fill_point - 0.5f * fill_bounds_extent - region_center; + const float2 region_offset = float2( + fill_point.x < bounds_min.x ? + offset_min.x : + (fill_point.x > bounds_max.x ? offset_max.x : offset_center.x), + fill_point.y < bounds_min.y ? + offset_min.y : + (fill_point.y > bounds_max.y ? offset_max.y : offset_center.y)); + const float2 offset = math::safe_divide(region_offset, region_extent); + + return std::make_pair(zoom, offset); + } + } + + return std::make_pair(float2(1.0f), float2(0.0f)); +} + +bke::CurvesGeometry fill_strokes(const ViewContext &view_context, + const Brush &brush, + const Scene &scene, + const bke::greasepencil::Layer &layer, + const VArray &boundary_layers, + const Span src_drawings, + const bool invert, + const float2 &fill_point, + const FillToolFitMethod fit_method, + const int stroke_material_index, + const bool keep_images) +{ + using bke::greasepencil::Layer; + + ARegion ®ion = *view_context.region; + View3D &view3d = *view_context.v3d; + RegionView3D &rv3d = *view_context.rv3d; + Depsgraph &depsgraph = *view_context.depsgraph; + Object &object = *view_context.obact; + + BLI_assert(object.type == OB_GREASE_PENCIL); + GreasePencil &grease_pencil = *static_cast(object.data); + const Object &object_eval = *DEG_get_evaluated_object(&depsgraph, &object); + + // TODO based on the fill_factor (aka "Precision") setting. + constexpr const int min_window_size = 128; + const float pixel_scale = 1.0f; + const int2 win_size = math::max(int2(region.winx, region.winy) * pixel_scale, + int2(min_window_size)); + const float2 win_center = 0.5f * float2(win_size); + + /* Zoom and offset based on bounds, to fit all strokes within the render. */ + const bool uniform_zoom = true; + const float max_zoom_factor = 5.0f; + const float2 margin = float2(20); + const auto [zoom, offset] = fit_strokes_to_view(view_context, + boundary_layers, + src_drawings, + fit_method, + fill_point, + uniform_zoom, + max_zoom_factor, + margin); + /* Fill point needs to be inverse transformed to stay relative to the view. */ + const float2 fill_point_view = math::safe_divide( + fill_point - win_center - offset * float2(win_size), zoom) + + win_center; + + image_render::RegionViewData region_view_data = image_render::region_init(region, win_size); + + GPUOffScreen *offscreen_buffer = image_render::image_render_begin(win_size); + GPU_blend(GPU_BLEND_ALPHA); + GPU_depth_mask(true); + image_render::set_viewmat(view_context, scene, win_size, zoom, offset); + + const eGP_FillDrawModes fill_draw_mode = GP_FILL_DMODE_BOTH; + const float alpha_threshold = 0.2f; + const bool brush_fill_hide = false; + const bool use_xray = false; + const bool fill_strokes = false; + + const float4x4 layer_to_world = layer.to_world_space(object); + ed::greasepencil::DrawingPlacement placement(scene, region, view3d, object_eval, layer); + const float3 fill_point_world = math::transform_point(layer_to_world, + placement.project(fill_point_view)); + + /* Draw blue point where click with mouse. */ + const float mouse_dot_size = 4.0f; + image_render::draw_dot(fill_point_world, mouse_dot_size, draw_seed_color); + + for (const DrawingInfo &info : src_drawings) { + const Layer &layer = *grease_pencil.layers()[info.layer_index]; + if (!layer.is_visible()) { + continue; + } + const float4x4 layer_to_world = layer.to_world_space(object); + const bool is_boundary_layer = boundary_layers[info.layer_index]; + const bke::CurvesGeometry &strokes = info.drawing.strokes(); + const bke::AttributeAccessor attributes = strokes.attributes(); + const VArray opacities = info.drawing.opacities(); + const VArray materials = *attributes.lookup(attr_material_index, + bke::AttrDomain::Curve); + + IndexMaskMemory curve_mask_memory; + const IndexMask curve_mask = get_visible_boundary_strokes( + object, info, is_boundary_layer, curve_mask_memory); + + const VArray colors = stroke_colors(object, + info.drawing.strokes(), + opacities, + materials, + draw_boundary_color, + alpha_threshold, + brush_fill_hide); + + image_render::draw_grease_pencil_strokes(rv3d, + win_size, + object, + info.drawing, + curve_mask, + colors, + layer_to_world, + fill_draw_mode, + use_xray, + fill_strokes); + } + + image_render::clear_viewmat(); + GPU_depth_mask(false); + GPU_blend(GPU_BLEND_NONE); + Image *ima = image_render::image_render_end(*view_context.bmain, offscreen_buffer); + if (!ima) { + return {}; + } + + /* TODO should use the same hardness as the paint tool. */ + const float stroke_hardness = 1.0f; + + bke::CurvesGeometry fill_curves = process_image(*ima, + scene, + view_context, + brush, + placement, + stroke_material_index, + stroke_hardness, + invert, + keep_images); + + /* Note: Region view reset has to happen after final curve construction, otherwise the curve + * placement, used to re-project from the 2D pixel coordinates, will have the wrong view + * transform. */ + image_render::region_reset(region, region_view_data); + + if (!keep_images) { + BKE_id_free(view_context.bmain, ima); + } + + return fill_curves; +} + +} // 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 43248b4abe6..f1e299a6da5 100644 --- a/source/blender/editors/space_view3d/space_view3d.cc +++ b/source/blender/editors/space_view3d/space_view3d.cc @@ -418,6 +418,14 @@ static void view3d_main_region_init(wmWindowManager *wm, ARegion *region) wm->defaultconf, "Grease Pencil Weight Paint", SPACE_EMPTY, RGN_TYPE_WINDOW); WM_event_add_keymap_handler(®ion->handlers, keymap); + keymap = WM_keymap_ensure( + wm->defaultconf, "Grease Pencil Brush Stroke", SPACE_EMPTY, RGN_TYPE_WINDOW); + WM_event_add_keymap_handler(®ion->handlers, keymap); + + keymap = WM_keymap_ensure( + wm->defaultconf, "Grease Pencil Fill Tool", SPACE_EMPTY, RGN_TYPE_WINDOW); + WM_event_add_keymap_handler(®ion->handlers, keymap); + /* Edit-font key-map swallows almost all (because of text input). */ keymap = WM_keymap_ensure(wm->defaultconf, "Font", SPACE_EMPTY, RGN_TYPE_WINDOW); WM_event_add_keymap_handler(®ion->handlers, keymap);