/* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * The Original Code is Copyright (C) 2015, Blender Foundation * This is a new part of Blender * Brush based operators for editing Grease Pencil strokes */ /** \file * \ingroup edgpencil */ #include "MEM_guardedalloc.h" #include "BLI_blenlib.h" #include "BLI_math.h" #include "BLT_translation.h" #include "DNA_brush_types.h" #include "DNA_gpencil_types.h" #include "BKE_brush.h" #include "BKE_colortools.h" #include "BKE_context.h" #include "BKE_gpencil.h" #include "BKE_material.h" #include "BKE_report.h" #include "WM_api.h" #include "WM_types.h" #include "RNA_access.h" #include "RNA_define.h" #include "UI_view2d.h" #include "ED_gpencil.h" #include "ED_screen.h" #include "ED_view3d.h" #include "DEG_depsgraph.h" #include "DEG_depsgraph_query.h" #include "gpencil_intern.h" /* ************************************************ */ /* General Brush Editing Context */ #define GP_SELECT_BUFFER_CHUNK 256 #define GP_GRID_PIXEL_SIZE 10.0f /* Temp Flags while Painting. */ typedef enum eGPDvertex_brush_Flag { /* invert the effect of the brush */ GP_VERTEX_FLAG_INVERT = (1 << 0), /* temporary invert action */ GP_VERTEX_FLAG_TMP_INVERT = (1 << 1), } eGPDvertex_brush_Flag; /* Grid of Colors for Smear. */ typedef struct tGP_Grid { /** Lower right corner of rectangle of grid cell. */ float bottom[2]; /** Upper left corner of rectangle of grid cell. */ float top[2]; /** Average Color */ float color[4]; /** Total points included. */ int totcol; } tGP_Grid; /* List of points affected by brush. */ typedef struct tGP_Selected { /** Referenced stroke. */ bGPDstroke *gps; /** Point index in points array. */ int pt_index; /** Position */ int pc[2]; /** Color */ float color[4]; } tGP_Selected; /* Context for brush operators */ typedef struct tGP_BrushVertexpaintData { Scene *scene; Object *object; ARegion *region; /* Current GPencil datablock */ bGPdata *gpd; Brush *brush; float linear_color[3]; eGPDvertex_brush_Flag flag; eGP_Vertex_SelectMaskFlag mask; /* Space Conversion Data */ GP_SpaceConversion gsc; /* Is the brush currently painting? */ bool is_painting; /* Start of new paint */ bool first; /* Is multiframe editing enabled, and are we using falloff for that? */ bool is_multiframe; bool use_multiframe_falloff; /* Brush Runtime Data: */ /* - position and pressure * - the *_prev variants are the previous values */ float mval[2], mval_prev[2]; float pressure, pressure_prev; /* - Effect 2D vector */ float dvec[2]; /* - multiframe falloff factor */ float mf_falloff; /* brush geometry (bounding box) */ rcti brush_rect; /* Temp data to save selected points */ /** Stroke buffer. */ tGP_Selected *pbuffer; /** Number of elements currently used in cache. */ int pbuffer_used; /** Number of total elements available in cache. */ int pbuffer_size; /** Grid of average colors */ tGP_Grid *grid; /** Total number of rows/cols. */ int grid_size; /** Total number of cells elments in the grid array. */ int grid_len; /** Grid sample position (used to determine distance of falloff) */ int grid_sample[2]; /** Grid is ready to use */ bool grid_ready; } tGP_BrushVertexpaintData; /* Ensure the buffer to hold temp selected point size is enough to save all points selected. */ static tGP_Selected *gpencil_select_buffer_ensure(tGP_Selected *buffer_array, int *buffer_size, int *buffer_used, const bool clear) { tGP_Selected *p = NULL; /* By default a buffer is created with one block with a predefined number of free slots, * if the size is not enough, the cache is reallocated adding a new block of free slots. * This is done in order to keep cache small and improve speed. */ if (*buffer_used + 1 > *buffer_size) { if ((*buffer_size == 0) || (buffer_array == NULL)) { p = MEM_callocN(sizeof(struct tGP_Selected) * GP_SELECT_BUFFER_CHUNK, __func__); *buffer_size = GP_SELECT_BUFFER_CHUNK; } else { *buffer_size += GP_SELECT_BUFFER_CHUNK; p = MEM_recallocN(buffer_array, sizeof(struct tGP_Selected) * *buffer_size); } if (p == NULL) { *buffer_size = *buffer_used = 0; } buffer_array = p; } /* clear old data */ if (clear) { *buffer_used = 0; if (buffer_array != NULL) { memset(buffer_array, 0, sizeof(tGP_Selected) * *buffer_size); } } return buffer_array; } /* Brush Operations ------------------------------- */ /* Invert behavior of brush? */ static bool brush_invert_check(tGP_BrushVertexpaintData *gso) { /* The basic setting is no inverted */ bool invert = false; /* During runtime, the user can hold down the Ctrl key to invert the basic behavior */ if (gso->flag & GP_VERTEX_FLAG_INVERT) { invert ^= true; } return invert; } /* Compute strength of effect. */ static float brush_influence_calc(tGP_BrushVertexpaintData *gso, const int radius, const int co[2]) { Brush *brush = gso->brush; float influence = brush->size; /* use pressure? */ if (brush->gpencil_settings->flag & GP_BRUSH_USE_PRESSURE) { influence *= gso->pressure; } /* distance fading */ int mval_i[2]; round_v2i_v2fl(mval_i, gso->mval); float distance = (float)len_v2v2_int(mval_i, co); /* Apply Brush curve. */ float brush_fallof = BKE_brush_curve_strength(brush, distance, (float)radius); influence *= brush_fallof; /* apply multiframe falloff */ influence *= gso->mf_falloff; /* return influence */ return influence; } /* Compute effect vector for directional brushes. */ static void brush_calc_dvec_2d(tGP_BrushVertexpaintData *gso) { gso->dvec[0] = (float)(gso->mval[0] - gso->mval_prev[0]); gso->dvec[1] = (float)(gso->mval[1] - gso->mval_prev[1]); normalize_v2(gso->dvec); } /* Init a grid of cells around mouse position. * * For each Cell. * * *--------* Top * | | * | | * Bottom *--------* * * The number of cells is calculated using the brush size and a predefined * number of pixels (see: GP_GRID_PIXEL_SIZE) */ static void gpencil_grid_cells_init(tGP_BrushVertexpaintData *gso) { tGP_Grid *grid; float bottom[2]; float top[2]; int grid_index = 0; /* The grid center is (0,0). */ bottom[0] = gso->brush_rect.xmin - gso->mval[0]; bottom[1] = gso->brush_rect.ymax - GP_GRID_PIXEL_SIZE - gso->mval[1]; /* Calc all cell of the grid from top/left. */ for (int y = gso->grid_size - 1; y >= 0; y--) { top[1] = bottom[1] + GP_GRID_PIXEL_SIZE; for (int x = 0; x < gso->grid_size; x++) { top[0] = bottom[0] + GP_GRID_PIXEL_SIZE; grid = &gso->grid[grid_index]; copy_v2_v2(grid->bottom, bottom); copy_v2_v2(grid->top, top); bottom[0] += GP_GRID_PIXEL_SIZE; grid_index++; } /* Reset for new row. */ bottom[0] = gso->brush_rect.xmin - gso->mval[0]; bottom[1] -= GP_GRID_PIXEL_SIZE; } } /* Get the index used in the grid base on dvec. */ static void gpencil_grid_cell_average_color_idx_get(tGP_BrushVertexpaintData *gso, int r_idx[2]) { /* Lower direction. */ if (gso->dvec[1] < 0.0f) { if ((gso->dvec[0] >= -1.0f) && (gso->dvec[0] < -0.8f)) { r_idx[0] = 0; r_idx[1] = -1; } else if ((gso->dvec[0] >= -0.8f) && (gso->dvec[0] < -0.6f)) { r_idx[0] = -1; r_idx[1] = -1; } else if ((gso->dvec[0] >= -0.6f) && (gso->dvec[0] < 0.6f)) { r_idx[0] = -1; r_idx[1] = 0; } else if ((gso->dvec[0] >= 0.6f) && (gso->dvec[0] < 0.8f)) { r_idx[0] = -1; r_idx[1] = 1; } else if (gso->dvec[0] >= 0.8f) { r_idx[0] = 0; r_idx[1] = 1; } } /* Upper direction. */ else { if ((gso->dvec[0] >= -1.0f) && (gso->dvec[0] < -0.8f)) { r_idx[0] = 0; r_idx[1] = -1; } else if ((gso->dvec[0] >= -0.8f) && (gso->dvec[0] < -0.6f)) { r_idx[0] = 1; r_idx[1] = -1; } else if ((gso->dvec[0] >= -0.6f) && (gso->dvec[0] < 0.6f)) { r_idx[0] = 1; r_idx[1] = 0; } else if ((gso->dvec[0] >= 0.6f) && (gso->dvec[0] < 0.8f)) { r_idx[0] = 1; r_idx[1] = 1; } else if (gso->dvec[0] >= 0.8f) { r_idx[0] = 0; r_idx[1] = 1; } } } static int gpencil_grid_cell_index_get(tGP_BrushVertexpaintData *gso, int pc[2]) { float bottom[2], top[2]; for (int i = 0; i < gso->grid_len; i++) { tGP_Grid *grid = &gso->grid[i]; add_v2_v2v2(bottom, grid->bottom, gso->mval); add_v2_v2v2(top, grid->top, gso->mval); if (pc[0] >= bottom[0] && pc[0] <= top[0] && pc[1] >= bottom[1] && pc[1] <= top[1]) { return i; } } return -1; } /* Fill the grid with the color in each cell and assign point cell index. */ static void gpencil_grid_colors_calc(tGP_BrushVertexpaintData *gso) { tGP_Selected *selected = NULL; bGPDstroke *gps_selected = NULL; bGPDspoint *pt = NULL; tGP_Grid *grid = NULL; /* Don't calculate again. */ if (gso->grid_ready) { return; } /* Extract colors by cell. */ for (int i = 0; i < gso->pbuffer_used; i++) { selected = &gso->pbuffer[i]; gps_selected = selected->gps; pt = &gps_selected->points[selected->pt_index]; int grid_index = gpencil_grid_cell_index_get(gso, selected->pc); if (grid_index > -1) { grid = &gso->grid[grid_index]; /* Add stroke mix color (only if used). */ if (pt->vert_color[3] > 0.0f) { add_v3_v3(grid->color, selected->color); grid->color[3] = 1.0f; grid->totcol++; } } } /* Average colors. */ for (int i = 0; i < gso->grid_len; i++) { grid = &gso->grid[i]; if (grid->totcol > 0) { mul_v3_fl(grid->color, (1.0f / (float)grid->totcol)); } } /* Save sample position. */ round_v2i_v2fl(gso->grid_sample, gso->mval); gso->grid_ready = true; return; } /* ************************************************ */ /* Brush Callbacks * This section defines the callbacks used by each brush to perform their magic. * These are called on each point within the brush's radius. */ /* Tint Brush */ static bool brush_tint_apply(tGP_BrushVertexpaintData *gso, bGPDstroke *gps, int pt_index, const int radius, const int co[2]) { Brush *brush = gso->brush; /* Attenuate factor to get a smoother tinting. */ float inf = (brush_influence_calc(gso, radius, co) * brush->gpencil_settings->draw_strength) / 100.0f; float inf_fill = (gso->pressure * brush->gpencil_settings->draw_strength) / 1000.0f; CLAMP(inf, 0.0f, 1.0f); CLAMP(inf_fill, 0.0f, 1.0f); /* Apply color to Stroke point. */ if (GPENCIL_TINT_VERTEX_COLOR_STROKE(brush) && (pt_index > -1)) { bGPDspoint *pt = &gps->points[pt_index]; if (brush_invert_check(gso)) { pt->vert_color[3] -= inf; CLAMP_MIN(pt->vert_color[3], 0.0f); } else { /* Premult. */ mul_v3_fl(pt->vert_color, pt->vert_color[3]); /* "Alpha over" blending. */ interp_v3_v3v3(pt->vert_color, pt->vert_color, gso->linear_color, inf); pt->vert_color[3] = pt->vert_color[3] * (1.0 - inf) + inf; /* Un-premult. */ if (pt->vert_color[3] > 0.0f) { mul_v3_fl(pt->vert_color, 1.0f / pt->vert_color[3]); } } } /* Apply color to Fill area (all with same color and factor). */ if (GPENCIL_TINT_VERTEX_COLOR_FILL(brush)) { if (brush_invert_check(gso)) { gps->vert_color_fill[3] -= inf_fill; CLAMP_MIN(gps->vert_color_fill[3], 0.0f); } else { /* Premult. */ mul_v3_fl(gps->vert_color_fill, gps->vert_color_fill[3]); /* "Alpha over" blending. */ interp_v3_v3v3(gps->vert_color_fill, gps->vert_color_fill, gso->linear_color, inf_fill); gps->vert_color_fill[3] = gps->vert_color_fill[3] * (1.0 - inf_fill) + inf_fill; /* Un-premult. */ if (gps->vert_color_fill[3] > 0.0f) { mul_v3_fl(gps->vert_color_fill, 1.0f / gps->vert_color_fill[3]); } } } return true; } /* Replace Brush (Don't use pressure or invert). */ static bool brush_replace_apply(tGP_BrushVertexpaintData *gso, bGPDstroke *gps, int pt_index) { Brush *brush = gso->brush; bGPDspoint *pt = &gps->points[pt_index]; /* Apply color to Stroke point. */ if (GPENCIL_TINT_VERTEX_COLOR_STROKE(brush)) { if (pt->vert_color[3] > 0.0f) { copy_v3_v3(pt->vert_color, gso->linear_color); } } /* Apply color to Fill area (all with same color and factor). */ if (GPENCIL_TINT_VERTEX_COLOR_FILL(brush)) { if (gps->vert_color_fill[3] > 0.0f) { copy_v3_v3(gps->vert_color_fill, gso->linear_color); } } return true; } /* Get surrounding color. */ static bool get_surrounding_color(tGP_BrushVertexpaintData *gso, bGPDstroke *gps, int pt_index, float r_color[3]) { tGP_Selected *selected = NULL; bGPDstroke *gps_selected = NULL; bGPDspoint *pt = NULL; int totcol = 0; zero_v3(r_color); /* Average the surrounding points except current one. */ for (int i = 0; i < gso->pbuffer_used; i++) { selected = &gso->pbuffer[i]; gps_selected = selected->gps; /* current point is not evaluated. */ if ((gps_selected == gps) && (selected->pt_index == pt_index)) { continue; } pt = &gps_selected->points[selected->pt_index]; /* Add stroke mix color (only if used). */ if (pt->vert_color[3] > 0.0f) { add_v3_v3(r_color, selected->color); totcol++; } } if (totcol > 0) { mul_v3_fl(r_color, (1.0f / (float)totcol)); return true; } return false; } /* Blur Brush */ static bool brush_blur_apply(tGP_BrushVertexpaintData *gso, bGPDstroke *gps, int pt_index, const int radius, const int co[2]) { Brush *brush = gso->brush; /* Attenuate factor to get a smoother tinting. */ float inf = (brush_influence_calc(gso, radius, co) * brush->gpencil_settings->draw_strength) / 100.0f; float inf_fill = (gso->pressure * brush->gpencil_settings->draw_strength) / 1000.0f; bGPDspoint *pt = &gps->points[pt_index]; /* Get surrounding color. */ float blur_color[3]; if (get_surrounding_color(gso, gps, pt_index, blur_color)) { /* Apply color to Stroke point. */ if (GPENCIL_TINT_VERTEX_COLOR_STROKE(brush)) { interp_v3_v3v3(pt->vert_color, pt->vert_color, blur_color, inf); } /* Apply color to Fill area (all with same color and factor). */ if (GPENCIL_TINT_VERTEX_COLOR_FILL(brush)) { interp_v3_v3v3(gps->vert_color_fill, gps->vert_color_fill, blur_color, inf_fill); } return true; } return false; } /* Average Brush */ static bool brush_average_apply(tGP_BrushVertexpaintData *gso, bGPDstroke *gps, int pt_index, const int radius, const int co[2], float average_color[3]) { Brush *brush = gso->brush; /* Attenuate factor to get a smoother tinting. */ float inf = (brush_influence_calc(gso, radius, co) * brush->gpencil_settings->draw_strength) / 100.0f; float inf_fill = (gso->pressure * brush->gpencil_settings->draw_strength) / 1000.0f; bGPDspoint *pt = &gps->points[pt_index]; float alpha = pt->vert_color[3]; float alpha_fill = gps->vert_color_fill[3]; if (brush_invert_check(gso)) { alpha -= inf; alpha_fill -= inf_fill; } else { alpha += inf; alpha_fill += inf_fill; } /* Apply color to Stroke point. */ if (GPENCIL_TINT_VERTEX_COLOR_STROKE(brush)) { CLAMP(alpha, 0.0f, 1.0f); interp_v3_v3v3(pt->vert_color, pt->vert_color, average_color, inf); pt->vert_color[3] = alpha; } /* Apply color to Fill area (all with same color and factor). */ if (GPENCIL_TINT_VERTEX_COLOR_FILL(brush)) { CLAMP(alpha_fill, 0.0f, 1.0f); copy_v3_v3(gps->vert_color_fill, average_color); gps->vert_color_fill[3] = alpha_fill; } return true; } /* Smear Brush */ static bool brush_smear_apply(tGP_BrushVertexpaintData *gso, bGPDstroke *gps, int pt_index, tGP_Selected *selected) { Brush *brush = gso->brush; tGP_Grid *grid = NULL; int average_idx[2]; ARRAY_SET_ITEMS(average_idx, 0, 0); bool changed = false; /* Need some movement, so first input is not done. */ if (gso->first) { return false; } bGPDspoint *pt = &gps->points[pt_index]; /* Need get average colors in the grid. */ if ((!gso->grid_ready) && (gso->pbuffer_used > 0)) { gpencil_grid_colors_calc(gso); } /* The influence is equal to strength and no decay around brush radius. */ float inf = brush->gpencil_settings->draw_strength; if (brush->flag & GP_BRUSH_USE_PRESSURE) { inf *= gso->pressure; } /* Calc distance from initial sample location and add a fallof effect. */ int mval_i[2]; round_v2i_v2fl(mval_i, gso->mval); float distance = (float)len_v2v2_int(mval_i, gso->grid_sample); float fac = 1.0f - (distance / (float)(brush->size * 2)); CLAMP(fac, 0.0f, 1.0f); inf *= fac; /* Retry row and col for average color. */ gpencil_grid_cell_average_color_idx_get(gso, average_idx); /* Retry average color cell. */ int grid_index = gpencil_grid_cell_index_get(gso, selected->pc); if (grid_index > -1) { int row = grid_index / gso->grid_size; int col = grid_index - (gso->grid_size * row); row += average_idx[0]; col += average_idx[1]; CLAMP(row, 0, gso->grid_size); CLAMP(col, 0, gso->grid_size); int new_index = (row * gso->grid_size) + col; CLAMP(new_index, 0, gso->grid_len - 1); grid = &gso->grid[new_index]; } /* Apply color to Stroke point. */ if (GPENCIL_TINT_VERTEX_COLOR_STROKE(brush)) { if (grid_index > -1) { if (grid->color[3] > 0.0f) { // copy_v3_v3(pt->vert_color, grid->color); interp_v3_v3v3(pt->vert_color, pt->vert_color, grid->color, inf); changed = true; } } } /* Apply color to Fill area (all with same color and factor). */ if (GPENCIL_TINT_VERTEX_COLOR_FILL(brush)) { if (grid_index > -1) { if (grid->color[3] > 0.0f) { interp_v3_v3v3(gps->vert_color_fill, gps->vert_color_fill, grid->color, inf); changed = true; } } } return changed; } /* ************************************************ */ /* Header Info */ static void gpencil_vertexpaint_brush_header_set(bContext *C) { ED_workspace_status_text(C, TIP_("GPencil Vertex Paint: LMB to paint | RMB/Escape to Exit" " | Ctrl to Invert Action")); } /* ************************************************ */ /* Grease Pencil Vertex Paint Operator */ /* Init/Exit ----------------------------------------------- */ static bool gpencil_vertexpaint_brush_init(bContext *C, wmOperator *op) { Scene *scene = CTX_data_scene(C); ToolSettings *ts = CTX_data_tool_settings(C); Object *ob = CTX_data_active_object(C); Paint *paint = ob->mode == OB_MODE_VERTEX_GPENCIL ? &ts->gp_vertexpaint->paint : &ts->gp_paint->paint; /* set the brush using the tool */ tGP_BrushVertexpaintData *gso; /* setup operator data */ gso = MEM_callocN(sizeof(tGP_BrushVertexpaintData), "tGP_BrushVertexpaintData"); op->customdata = gso; gso->brush = paint->brush; srgb_to_linearrgb_v3_v3(gso->linear_color, gso->brush->rgb); BKE_curvemapping_initialize(gso->brush->curve); gso->is_painting = false; gso->first = true; gso->pbuffer = NULL; gso->pbuffer_size = 0; gso->pbuffer_used = 0; /* Alloc grid array */ gso->grid_size = (int)(((gso->brush->size * 2.0f) / GP_GRID_PIXEL_SIZE) + 1.0); /* Square value. */ gso->grid_len = gso->grid_size * gso->grid_size; gso->grid = MEM_callocN(sizeof(tGP_Grid) * gso->grid_len, "tGP_Grid"); gso->grid_ready = false; gso->gpd = ED_gpencil_data_get_active(C); gso->scene = scene; gso->object = ob; gso->region = CTX_wm_region(C); /* Save mask. */ gso->mask = ts->gpencil_selectmode_vertex; /* Multiframe settings. */ gso->is_multiframe = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gso->gpd); gso->use_multiframe_falloff = (ts->gp_sculpt.flag & GP_SCULPT_SETT_FLAG_FRAME_FALLOFF) != 0; /* Init multi-edit falloff curve data before doing anything, * so we won't have to do it again later. */ if (gso->is_multiframe) { BKE_curvemapping_initialize(ts->gp_sculpt.cur_falloff); } /* Setup space conversions. */ gpencil_point_conversion_init(C, &gso->gsc); /* Update header. */ gpencil_vertexpaint_brush_header_set(C); return true; } static void gpencil_vertexpaint_brush_exit(bContext *C, wmOperator *op) { tGP_BrushVertexpaintData *gso = op->customdata; /* Disable headerprints. */ ED_workspace_status_text(C, NULL); /* Disable temp invert flag. */ gso->brush->flag &= ~GP_VERTEX_FLAG_TMP_INVERT; /* Free operator data */ MEM_SAFE_FREE(gso->pbuffer); MEM_SAFE_FREE(gso->grid); MEM_SAFE_FREE(gso); op->customdata = NULL; } /* Poll callback for stroke vertex paint operator. */ static bool gpencil_vertexpaint_brush_poll(bContext *C) { /* NOTE: this is a bit slower, but is the most accurate... */ return CTX_DATA_COUNT(C, editable_gpencil_strokes) != 0; } /* Helper to save the points selected by the brush. */ static void gpencil_save_selected_point(tGP_BrushVertexpaintData *gso, bGPDstroke *gps, int index, int pc[2]) { tGP_Selected *selected; bGPDspoint *pt = &gps->points[index]; /* Ensure the array to save the list of selected points is big enough. */ gso->pbuffer = gpencil_select_buffer_ensure( gso->pbuffer, &gso->pbuffer_size, &gso->pbuffer_used, false); selected = &gso->pbuffer[gso->pbuffer_used]; selected->gps = gps; selected->pt_index = index; /* Check the index is not a special case for fill. */ if (index > -1) { copy_v2_v2_int(selected->pc, pc); copy_v4_v4(selected->color, pt->vert_color); } gso->pbuffer_used++; } /* Select points in this stroke and add to an array to be used later. */ static void gpencil_vertexpaint_select_stroke(tGP_BrushVertexpaintData *gso, bGPDstroke *gps, const char tool, const float diff_mat[4][4]) { GP_SpaceConversion *gsc = &gso->gsc; rcti *rect = &gso->brush_rect; Brush *brush = gso->brush; const int radius = (brush->flag & GP_BRUSH_USE_PRESSURE) ? gso->brush->size * gso->pressure : gso->brush->size; bGPDstroke *gps_active = (gps->runtime.gps_orig) ? gps->runtime.gps_orig : gps; bGPDspoint *pt_active = NULL; bGPDspoint *pt1, *pt2; bGPDspoint *pt = NULL; int pc1[2] = {0}; int pc2[2] = {0}; int i; int index; bool include_last = false; /* Check if the stroke collide with brush. */ if (!ED_gpencil_stroke_check_collision(gsc, gps, gso->mval, radius, diff_mat)) { return; } if (gps->totpoints == 1) { bGPDspoint pt_temp; pt = &gps->points[0]; gpencil_point_to_parent_space(gps->points, diff_mat, &pt_temp); gpencil_point_to_xy(gsc, gps, &pt_temp, &pc1[0], &pc1[1]); pt_active = (pt->runtime.pt_orig) ? pt->runtime.pt_orig : pt; /* do boundbox check first */ if ((!ELEM(V2D_IS_CLIPPED, pc1[0], pc1[1])) && BLI_rcti_isect_pt(rect, pc1[0], pc1[1])) { /* only check if point is inside */ int mval_i[2]; round_v2i_v2fl(mval_i, gso->mval); if (len_v2v2_int(mval_i, pc1) <= radius) { /* apply operation to this point */ if (pt_active != NULL) { gpencil_save_selected_point(gso, gps_active, 0, pc1); } } } } else { /* Loop over the points in the stroke, checking for intersections * - an intersection means that we touched the stroke */ bool hit = false; for (i = 0; (i + 1) < gps->totpoints; i++) { /* Get points to work with */ pt1 = gps->points + i; pt2 = gps->points + i + 1; /* Skip if neither one is selected * (and we are only allowed to edit/consider selected points) */ if (GPENCIL_ANY_VERTEX_MASK(gso->mask)) { if (!(pt1->flag & GP_SPOINT_SELECT) && !(pt2->flag & GP_SPOINT_SELECT)) { include_last = false; continue; } } bGPDspoint npt; gpencil_point_to_parent_space(pt1, diff_mat, &npt); gpencil_point_to_xy(gsc, gps, &npt, &pc1[0], &pc1[1]); gpencil_point_to_parent_space(pt2, diff_mat, &npt); gpencil_point_to_xy(gsc, gps, &npt, &pc2[0], &pc2[1]); /* Check that point segment of the boundbox of the selection stroke */ if (((!ELEM(V2D_IS_CLIPPED, pc1[0], pc1[1])) && BLI_rcti_isect_pt(rect, pc1[0], pc1[1])) || ((!ELEM(V2D_IS_CLIPPED, pc2[0], pc2[1])) && BLI_rcti_isect_pt(rect, pc2[0], pc2[1]))) { /* Check if point segment of stroke had anything to do with * brush region (either within stroke painted, or on its lines) * - this assumes that linewidth is irrelevant */ if (gpencil_stroke_inside_circle(gso->mval, radius, pc1[0], pc1[1], pc2[0], pc2[1])) { /* To each point individually... */ pt = &gps->points[i]; pt_active = (pt->runtime.pt_orig) ? pt->runtime.pt_orig : pt; index = (pt->runtime.pt_orig) ? pt->runtime.idx_orig : i; if (pt_active != NULL) { /* If masked and the point is not selected, skip it. */ if ((GPENCIL_ANY_VERTEX_MASK(gso->mask)) && ((pt_active->flag & GP_SPOINT_SELECT) == 0)) { continue; } hit = true; gpencil_save_selected_point(gso, gps_active, index, pc1); } /* Only do the second point if this is the last segment, * and it is unlikely that the point will get handled * otherwise. * * NOTE: There is a small risk here that the second point wasn't really * actually in-range. In that case, it only got in because * the line linking the points was! */ if (i + 1 == gps->totpoints - 1) { pt = &gps->points[i + 1]; pt_active = (pt->runtime.pt_orig) ? pt->runtime.pt_orig : pt; index = (pt->runtime.pt_orig) ? pt->runtime.idx_orig : i + 1; if (pt_active != NULL) { hit = true; gpencil_save_selected_point(gso, gps_active, index, pc2); include_last = false; } } else { include_last = true; } } else if (include_last) { /* This case is for cases where for whatever reason the second vert (1st here) * doesn't get included because the whole edge isn't in bounds, * but it would've qualified since it did with the previous step * (but wasn't added then, to avoid double-ups). */ pt = &gps->points[i]; pt_active = (pt->runtime.pt_orig) ? pt->runtime.pt_orig : pt; index = (pt->runtime.pt_orig) ? pt->runtime.idx_orig : i; if (pt_active != NULL) { hit = true; gpencil_save_selected_point(gso, gps_active, index, pc1); include_last = false; } } } } /* If nothing hit, check if the mouse is inside any filled stroke. */ if ((!hit) && (ELEM(tool, GPAINT_TOOL_TINT, GPVERTEX_TOOL_DRAW))) { MaterialGPencilStyle *gp_style = BKE_gpencil_material_settings(gso->object, gps_active->mat_nr + 1); if (gp_style->flag & GP_MATERIAL_FILL_SHOW) { int mval[2]; round_v2i_v2fl(mval, gso->mval); bool hit_fill = ED_gpencil_stroke_point_is_inside(gps_active, gsc, mval, diff_mat); if (hit_fill) { /* Need repeat the effect because if we don't do that the tint process * is very slow. */ for (int repeat = 0; repeat < 50; repeat++) { gpencil_save_selected_point(gso, gps_active, -1, NULL); } } } } } } /* Apply vertex paint brushes to strokes in the given frame. */ static bool gpencil_vertexpaint_brush_do_frame(bContext *C, tGP_BrushVertexpaintData *gso, bGPDlayer *gpl, bGPDframe *gpf, const float diff_mat[4][4]) { Object *ob = CTX_data_active_object(C); const char tool = ob->mode == OB_MODE_VERTEX_GPENCIL ? gso->brush->gpencil_vertex_tool : gso->brush->gpencil_tool; const int radius = (gso->brush->flag & GP_BRUSH_USE_PRESSURE) ? gso->brush->size * gso->pressure : gso->brush->size; tGP_Selected *selected = NULL; int i; /*--------------------------------------------------------------------- * First step: select the points affected. This step is required to have * all selected points before apply the effect, because it could be * required to average data. *--------------------------------------------------------------------- */ LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) { /* Skip strokes that are invalid for current view. */ if (ED_gpencil_stroke_can_use(C, gps) == false) { continue; } /* Check if the color is editable. */ if (ED_gpencil_stroke_color_use(ob, gpl, gps) == false) { continue; } /* Check points below the brush. */ gpencil_vertexpaint_select_stroke(gso, gps, tool, diff_mat); } /* For Average tool, need calculate the average resulting color from all colors * under the brush. */ float average_color[3] = {0}; int totcol = 0; if ((tool == GPVERTEX_TOOL_AVERAGE) && (gso->pbuffer_used > 0)) { for (i = 0; i < gso->pbuffer_used; i++) { selected = &gso->pbuffer[i]; bGPDstroke *gps = selected->gps; bGPDspoint *pt = &gps->points[selected->pt_index]; /* Add stroke mix color (only if used). */ if (pt->vert_color[3] > 0.0f) { add_v3_v3(average_color, pt->vert_color); totcol++; } /* If Fill color mix, add to average. */ if (gps->vert_color_fill[3] > 0.0f) { add_v3_v3(average_color, gps->vert_color_fill); totcol++; } } /* Get average. */ if (totcol > 0) { mul_v3_fl(average_color, (1.0f / (float)totcol)); } } /*--------------------------------------------------------------------- * Second step: Apply effect. *--------------------------------------------------------------------- */ bool changed = false; for (i = 0; i < gso->pbuffer_used; i++) { changed = true; selected = &gso->pbuffer[i]; switch (tool) { case GPAINT_TOOL_TINT: case GPVERTEX_TOOL_DRAW: { brush_tint_apply(gso, selected->gps, selected->pt_index, radius, selected->pc); changed |= true; break; } case GPVERTEX_TOOL_BLUR: { brush_blur_apply(gso, selected->gps, selected->pt_index, radius, selected->pc); changed |= true; break; } case GPVERTEX_TOOL_AVERAGE: { brush_average_apply( gso, selected->gps, selected->pt_index, radius, selected->pc, average_color); changed |= true; break; } case GPVERTEX_TOOL_SMEAR: { brush_smear_apply(gso, selected->gps, selected->pt_index, selected); changed |= true; break; } case GPVERTEX_TOOL_REPLACE: { brush_replace_apply(gso, selected->gps, selected->pt_index); changed |= true; break; } default: printf("ERROR: Unknown type of GPencil Vertex Paint brush\n"); break; } } /* Clear the selected array, but keep the memory allocation.*/ gso->pbuffer = gpencil_select_buffer_ensure( gso->pbuffer, &gso->pbuffer_size, &gso->pbuffer_used, true); return changed; } /* Apply brush effect to all layers. */ static bool gpencil_vertexpaint_brush_apply_to_layers(bContext *C, tGP_BrushVertexpaintData *gso) { ToolSettings *ts = CTX_data_tool_settings(C); Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); Object *obact = gso->object; bool changed = false; Object *ob_eval = (Object *)DEG_get_evaluated_id(depsgraph, &obact->id); bGPdata *gpd = (bGPdata *)ob_eval->data; /* Find visible strokes, and perform operations on those if hit */ LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { /* If locked or no active frame, don't do anything. */ if ((!BKE_gpencil_layer_is_editable(gpl)) || (gpl->actframe == NULL)) { continue; } /* calculate difference matrix */ float diff_mat[4][4]; BKE_gpencil_parent_matrix_get(depsgraph, obact, gpl, diff_mat); /* Active Frame or MultiFrame? */ if (gso->is_multiframe) { /* init multiframe falloff options */ int f_init = 0; int f_end = 0; if (gso->use_multiframe_falloff) { BKE_gpencil_frame_range_selected(gpl, &f_init, &f_end); } LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) { /* Always do active frame; Otherwise, only include selected frames */ if ((gpf == gpl->actframe) || (gpf->flag & GP_FRAME_SELECT)) { /* Compute multi-frame falloff factor. */ if (gso->use_multiframe_falloff) { /* Falloff depends on distance to active frame (relative to the overall frame range) */ gso->mf_falloff = BKE_gpencil_multiframe_falloff_calc( gpf, gpl->actframe->framenum, f_init, f_end, ts->gp_sculpt.cur_falloff); } else { /* No falloff */ gso->mf_falloff = 1.0f; } /* affect strokes in this frame */ changed |= gpencil_vertexpaint_brush_do_frame(C, gso, gpl, gpf, diff_mat); } } } else { /* Apply to active frame's strokes */ if (gpl->actframe != NULL) { gso->mf_falloff = 1.0f; changed |= gpencil_vertexpaint_brush_do_frame(C, gso, gpl, gpl->actframe, diff_mat); } } } return changed; } /* Calculate settings for applying brush */ static void gpencil_vertexpaint_brush_apply(bContext *C, wmOperator *op, PointerRNA *itemptr) { tGP_BrushVertexpaintData *gso = op->customdata; Brush *brush = gso->brush; const int radius = ((brush->flag & GP_BRUSH_USE_PRESSURE) ? gso->brush->size * gso->pressure : gso->brush->size); float mousef[2]; int mouse[2]; bool changed = false; /* Get latest mouse coordinates */ RNA_float_get_array(itemptr, "mouse", mousef); gso->mval[0] = mouse[0] = (int)(mousef[0]); gso->mval[1] = mouse[1] = (int)(mousef[1]); gso->pressure = RNA_float_get(itemptr, "pressure"); if (RNA_boolean_get(itemptr, "pen_flip")) { gso->flag |= GP_VERTEX_FLAG_INVERT; } else { gso->flag &= ~GP_VERTEX_FLAG_INVERT; } /* Store coordinates as reference, if operator just started running */ if (gso->first) { gso->mval_prev[0] = gso->mval[0]; gso->mval_prev[1] = gso->mval[1]; gso->pressure_prev = gso->pressure; } /* Update brush_rect, so that it represents the bounding rectangle of brush. */ gso->brush_rect.xmin = mouse[0] - radius; gso->brush_rect.ymin = mouse[1] - radius; gso->brush_rect.xmax = mouse[0] + radius; gso->brush_rect.ymax = mouse[1] + radius; /* Calc 2D direction vector and relative angle. */ brush_calc_dvec_2d(gso); /* Calc grid for smear tool. */ gpencil_grid_cells_init(gso); changed = gpencil_vertexpaint_brush_apply_to_layers(C, gso); /* Updates */ if (changed) { DEG_id_tag_update(&gso->gpd->id, ID_RECALC_GEOMETRY); WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL); } /* Store values for next step */ gso->mval_prev[0] = gso->mval[0]; gso->mval_prev[1] = gso->mval[1]; gso->pressure_prev = gso->pressure; gso->first = false; } /* Running --------------------------------------------- */ /* helper - a record stroke, and apply paint event */ static void gpencil_vertexpaint_brush_apply_event(bContext *C, wmOperator *op, const wmEvent *event) { tGP_BrushVertexpaintData *gso = op->customdata; PointerRNA itemptr; float mouse[2]; mouse[0] = event->mval[0] + 1; mouse[1] = event->mval[1] + 1; /* fill in stroke */ RNA_collection_add(op->ptr, "stroke", &itemptr); RNA_float_set_array(&itemptr, "mouse", mouse); RNA_boolean_set(&itemptr, "pen_flip", event->ctrl != false); RNA_boolean_set(&itemptr, "is_start", gso->first); /* Handle pressure sensitivity (which is supplied by tablets). */ float pressure = event->tablet.pressure; CLAMP(pressure, 0.0f, 1.0f); RNA_float_set(&itemptr, "pressure", pressure); /* apply */ gpencil_vertexpaint_brush_apply(C, op, &itemptr); } /* reapply */ static int gpencil_vertexpaint_brush_exec(bContext *C, wmOperator *op) { if (!gpencil_vertexpaint_brush_init(C, op)) { return OPERATOR_CANCELLED; } RNA_BEGIN (op->ptr, itemptr, "stroke") { gpencil_vertexpaint_brush_apply(C, op, &itemptr); } RNA_END; gpencil_vertexpaint_brush_exit(C, op); return OPERATOR_FINISHED; } /* start modal painting */ static int gpencil_vertexpaint_brush_invoke(bContext *C, wmOperator *op, const wmEvent *event) { tGP_BrushVertexpaintData *gso = NULL; const bool is_modal = RNA_boolean_get(op->ptr, "wait_for_input"); const bool is_playing = ED_screen_animation_playing(CTX_wm_manager(C)) != NULL; /* the operator cannot work while play animation */ if (is_playing) { BKE_report(op->reports, RPT_ERROR, "Cannot Paint while play animation"); return OPERATOR_CANCELLED; } /* init painting data */ if (!gpencil_vertexpaint_brush_init(C, op)) { return OPERATOR_CANCELLED; } gso = op->customdata; /* register modal handler */ WM_event_add_modal_handler(C, op); /* start drawing immediately? */ if (is_modal == false) { ARegion *region = CTX_wm_region(C); /* apply first dab... */ gso->is_painting = true; gpencil_vertexpaint_brush_apply_event(C, op, event); /* redraw view with feedback */ ED_region_tag_redraw(region); } return OPERATOR_RUNNING_MODAL; } /* painting - handle events */ static int gpencil_vertexpaint_brush_modal(bContext *C, wmOperator *op, const wmEvent *event) { tGP_BrushVertexpaintData *gso = op->customdata; const bool is_modal = RNA_boolean_get(op->ptr, "wait_for_input"); bool redraw_region = false; bool redraw_toolsettings = false; /* The operator can be in 2 states: Painting and Idling */ if (gso->is_painting) { /* Painting */ switch (event->type) { /* Mouse Move = Apply somewhere else */ case MOUSEMOVE: case INBETWEEN_MOUSEMOVE: /* apply brush effect at new position */ gpencil_vertexpaint_brush_apply_event(C, op, event); /* force redraw, so that the cursor will at least be valid */ redraw_region = true; break; /* Painting mbut release = Stop painting (back to idle) */ case LEFTMOUSE: if (is_modal) { /* go back to idling... */ gso->is_painting = false; } else { /* end painting, since we're not modal */ gso->is_painting = false; gpencil_vertexpaint_brush_exit(C, op); return OPERATOR_FINISHED; } break; /* Abort painting if any of the usual things are tried */ case MIDDLEMOUSE: case RIGHTMOUSE: case EVT_ESCKEY: gpencil_vertexpaint_brush_exit(C, op); return OPERATOR_FINISHED; } } else { /* Idling */ BLI_assert(is_modal == true); switch (event->type) { /* Painting mbut press = Start painting (switch to painting state) */ case LEFTMOUSE: /* do initial "click" apply */ gso->is_painting = true; gso->first = true; gpencil_vertexpaint_brush_apply_event(C, op, event); break; /* Exit modal operator, based on the "standard" ops */ case RIGHTMOUSE: case EVT_ESCKEY: gpencil_vertexpaint_brush_exit(C, op); return OPERATOR_FINISHED; /* MMB is often used for view manipulations */ case MIDDLEMOUSE: return OPERATOR_PASS_THROUGH; /* Mouse movements should update the brush cursor - Just redraw the active region */ case MOUSEMOVE: case INBETWEEN_MOUSEMOVE: redraw_region = true; break; /* Change Frame - Allowed */ case EVT_LEFTARROWKEY: case EVT_RIGHTARROWKEY: case EVT_UPARROWKEY: case EVT_DOWNARROWKEY: return OPERATOR_PASS_THROUGH; /* Camera/View Gizmo's - Allowed */ /* (See rationale in gpencil_paint.c -> gpencil_draw_modal()) */ case EVT_PAD0: case EVT_PAD1: case EVT_PAD2: case EVT_PAD3: case EVT_PAD4: case EVT_PAD5: case EVT_PAD6: case EVT_PAD7: case EVT_PAD8: case EVT_PAD9: return OPERATOR_PASS_THROUGH; /* Unhandled event */ default: break; } } /* Redraw region? */ if (redraw_region) { ED_region_tag_redraw(CTX_wm_region(C)); } /* Redraw toolsettings (brush settings)? */ if (redraw_toolsettings) { DEG_id_tag_update(&gso->gpd->id, ID_RECALC_GEOMETRY); WM_event_add_notifier(C, NC_SCENE | ND_TOOLSETTINGS, NULL); } return OPERATOR_RUNNING_MODAL; } void GPENCIL_OT_vertex_paint(wmOperatorType *ot) { /* identifiers */ ot->name = "Stroke Vertex Paint"; ot->idname = "GPENCIL_OT_vertex_paint"; ot->description = "Paint stroke points with a color"; /* api callbacks */ ot->exec = gpencil_vertexpaint_brush_exec; ot->invoke = gpencil_vertexpaint_brush_invoke; ot->modal = gpencil_vertexpaint_brush_modal; ot->cancel = gpencil_vertexpaint_brush_exit; ot->poll = gpencil_vertexpaint_brush_poll; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_BLOCKING; /* properties */ PropertyRNA *prop; prop = RNA_def_collection_runtime(ot->srna, "stroke", &RNA_OperatorStrokeElement, "Stroke", ""); RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); prop = RNA_def_boolean(ot->srna, "wait_for_input", true, "Wait for Input", ""); RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); }