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);