diff options
-rw-r--r-- | release/scripts/startup/bl_ui/properties_grease_pencil_common.py | 1 | ||||
-rw-r--r-- | release/scripts/startup/bl_ui/space_view3d.py | 2 | ||||
-rw-r--r-- | source/blender/editors/gpencil/CMakeLists.txt | 1 | ||||
-rw-r--r-- | source/blender/editors/gpencil/gpencil_intern.h | 5 | ||||
-rw-r--r-- | source/blender/editors/gpencil/gpencil_merge.c | 573 | ||||
-rw-r--r-- | source/blender/editors/gpencil/gpencil_ops.c | 1 |
6 files changed, 583 insertions, 0 deletions
diff --git a/release/scripts/startup/bl_ui/properties_grease_pencil_common.py b/release/scripts/startup/bl_ui/properties_grease_pencil_common.py index be335469418..8e29e03a8f6 100644 --- a/release/scripts/startup/bl_ui/properties_grease_pencil_common.py +++ b/release/scripts/startup/bl_ui/properties_grease_pencil_common.py @@ -217,6 +217,7 @@ class GreasePencilStrokeEditPanel: col.separator() row = col.row(align=True) + row.operator("gpencil.stroke_merge", text="Merge") row.operator("gpencil.stroke_join", text="Join").type = 'JOIN' row.operator("gpencil.stroke_join", text="& Copy").type = 'JOINCOPY' diff --git a/release/scripts/startup/bl_ui/space_view3d.py b/release/scripts/startup/bl_ui/space_view3d.py index 93f772785a5..9c2f25b4fb1 100644 --- a/release/scripts/startup/bl_ui/space_view3d.py +++ b/release/scripts/startup/bl_ui/space_view3d.py @@ -3935,6 +3935,7 @@ class VIEW3D_MT_edit_gpencil(Menu): layout.operator_menu_enum("gpencil.stroke_separate", "mode", text="Separate...") layout.operator("gpencil.stroke_split", text="Split") + layout.operator("gpencil.stroke_merge", text="Merge") layout.operator_menu_enum("gpencil.stroke_join", "type", text="Join...") layout.operator("gpencil.stroke_flip", text="Flip Direction") @@ -5558,6 +5559,7 @@ class VIEW3D_MT_gpencil_edit_specials(Menu): layout.separator() + layout.operator("gpencil.stroke_merge", text="Merge") layout.operator("gpencil.stroke_join", text="Join").type = 'JOIN' layout.operator("gpencil.stroke_join", text="Join & Copy").type = 'JOINCOPY' layout.operator("gpencil.stroke_flip", text="Flip Direction") diff --git a/source/blender/editors/gpencil/CMakeLists.txt b/source/blender/editors/gpencil/CMakeLists.txt index cea2e5d4269..576e16077d2 100644 --- a/source/blender/editors/gpencil/CMakeLists.txt +++ b/source/blender/editors/gpencil/CMakeLists.txt @@ -49,6 +49,7 @@ set(SRC gpencil_convert.c gpencil_data.c gpencil_edit.c + gpencil_merge.c gpencil_interpolate.c gpencil_primitive.c gpencil_ops.c diff --git a/source/blender/editors/gpencil/gpencil_intern.h b/source/blender/editors/gpencil/gpencil_intern.h index aaeb2603acc..6ce1d71db83 100644 --- a/source/blender/editors/gpencil/gpencil_intern.h +++ b/source/blender/editors/gpencil/gpencil_intern.h @@ -388,6 +388,10 @@ enum { GP_STROKE_CURVE = 4 }; +enum { + GP_MERGE_STROKE = -1, + GP_MERGE_POINT = 1 +}; void GPENCIL_OT_stroke_arrange(struct wmOperatorType *ot); void GPENCIL_OT_stroke_change_color(struct wmOperatorType *ot); @@ -402,6 +406,7 @@ void GPENCIL_OT_stroke_simplify_fixed(struct wmOperatorType *ot); void GPENCIL_OT_stroke_separate(struct wmOperatorType *ot); void GPENCIL_OT_stroke_split(struct wmOperatorType *ot); void GPENCIL_OT_stroke_smooth(struct wmOperatorType *ot); +void GPENCIL_OT_stroke_merge(struct wmOperatorType *ot); void GPENCIL_OT_brush_presets_create(struct wmOperatorType *ot); diff --git a/source/blender/editors/gpencil/gpencil_merge.c b/source/blender/editors/gpencil/gpencil_merge.c new file mode 100644 index 00000000000..02f71b19398 --- /dev/null +++ b/source/blender/editors/gpencil/gpencil_merge.c @@ -0,0 +1,573 @@ +/* + * ***** BEGIN GPL LICENSE BLOCK ***** + * + * 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) 2019, Blender Foundation. + * This is a new part of Blender + * + * Contributor(s): Antonio Vazquez + * + * ***** END GPL LICENSE BLOCK ***** + * + * Operators for merge Grease Pencil strokes + */ + + /** \file blender/editors/gpencil/gpencil_merge.c + * \ingroup edgpencil + */ + + +#include <stdio.h> + +#include "MEM_guardedalloc.h" + +#include "BLI_blenlib.h" +#include "BLI_ghash.h" +#include "BLI_math.h" + +#include "DNA_gpencil_types.h" + +#include "BKE_brush.h" +#include "BKE_context.h" +#include "BKE_gpencil.h" +#include "BKE_material.h" + +#include "WM_api.h" +#include "WM_types.h" + +#include "RNA_access.h" +#include "RNA_define.h" + +#include "ED_gpencil.h" +#include "ED_object.h" +#include "ED_screen.h" +#include "ED_view3d.h" + +#include "DEG_depsgraph.h" +#include "DEG_depsgraph_query.h" + +#include "gpencil_intern.h" + +typedef struct tGPencilPointCache { + float factor; /* value to sort */ + bGPDstroke *gps; + float x, y, z; + float pressure; + float strength; +} tGPencilPointCache; + +/* helper function to sort points */ +static int gpencil_sort_points(const void *a1, const void *a2) +{ + const tGPencilPointCache *ps1 = a1, *ps2 = a2; + + if (ps1->factor < ps2->factor) return -1; + else if (ps1->factor > ps2->factor) return 1; + + return 0; +} + +static void gpencil_insert_points_to_stroke( + bGPDstroke *gps, tGPencilPointCache *points_array, int totpoints) +{ + tGPencilPointCache *point_elem = NULL; + + for (int i = 0; i < totpoints; i++) { + point_elem = &points_array[i]; + bGPDspoint *pt_dst = &gps->points[i]; + + copy_v3_v3(&pt_dst->x, &point_elem->x); + pt_dst->pressure = point_elem->pressure; + pt_dst->strength = point_elem->strength; + pt_dst->uv_fac = 1.0f; + pt_dst->uv_rot = 0; + pt_dst->flag |= GP_SPOINT_SELECT; + } + +} + +static bGPDstroke *gpencil_prepare_stroke(bContext *C, wmOperator *op, int totpoints) +{ + ToolSettings *ts = CTX_data_tool_settings(C); + Depsgraph *depsgraph = CTX_data_depsgraph(C); + Object *ob = CTX_data_active_object(C); + bGPDlayer *gpl = CTX_data_active_gpencil_layer(C); + + int cfra_eval = (int)DEG_get_ctime(depsgraph); + + const bool back = RNA_boolean_get(op->ptr, "back"); + const bool additive = RNA_boolean_get(op->ptr, "additive"); + const bool cyclic = RNA_boolean_get(op->ptr, "cyclic"); + + Paint *paint = &ts->gp_paint->paint; + /* if not exist, create a new one */ + if (paint->brush == NULL) { + /* create new brushes */ + BKE_brush_gpencil_presets(C); + } + Brush *brush = paint->brush; + + /* frame */ + short add_frame_mode; + if (additive) { + add_frame_mode = GP_GETFRAME_ADD_COPY; + } + else { + add_frame_mode = GP_GETFRAME_ADD_NEW; + } + bGPDframe *gpf = BKE_gpencil_layer_getframe(gpl, cfra_eval, add_frame_mode); + + /* stroke */ + bGPDstroke *gps = MEM_callocN(sizeof(bGPDstroke), "gp_stroke"); + gps->totpoints = totpoints; + gps->inittime = 0.0f; + gps->thickness = brush->size; + gps->flag |= GP_STROKE_SELECT; + gps->flag |= GP_STROKE_3DSPACE; + gps->mat_nr = ob->actcol - 1; + + /* allocate memory for points */ + gps->points = MEM_callocN(sizeof(bGPDspoint) * totpoints, "gp_stroke_points"); + /* initialize triangle memory to dummy data */ + gps->tot_triangles = 0; + gps->triangles = NULL; + gps->flag |= GP_STROKE_RECALC_GEOMETRY; + + if (cyclic) { + gps->flag |= GP_STROKE_CYCLIC; + } + + /* add new stroke to frame */ + if (back) { + BLI_addhead(&gpf->strokes, gps); + } + else { + BLI_addtail(&gpf->strokes, gps); + } + + return gps; +} + +static void gpencil_get_elements_len(bContext *C, int *totstrokes, int *totpoints) +{ + bGPDspoint *pt; + int i; + + /* count number of strokes and selected points */ + CTX_DATA_BEGIN(C, bGPDstroke *, gps, editable_gpencil_strokes) + { + if (gps->flag & GP_STROKE_SELECT) { + *totstrokes += 1; + for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) { + if (pt->flag & GP_SPOINT_SELECT) { + *totpoints += 1; + } + } + } + } + CTX_DATA_END; +} + +static void gpencil_dissolve_points(bContext *C) +{ + bGPDstroke *gps, *gpsn; + + CTX_DATA_BEGIN(C, bGPDlayer *, gpl, editable_gpencil_layers) + { + bGPDframe *gpf = gpl->actframe; + if (gpf == NULL) { + continue; + } + + for (gps = gpf->strokes.first; gps; gps = gpsn) { + gpsn = gps->next; + gp_stroke_delete_tagged_points(gpf, gps, gpsn, GP_SPOINT_TAG, false); + } + } + CTX_DATA_END; +} + +/* Calc a factor of each selected point and fill an array with all the data. + * + * The factor is calculated using an imaginary circle, using the angle relative + * to this circle and the distance to the calculated center of the selected points. + * + * All the data is saved to be sorted and used later. + */ +static void gpencil_calc_points_factor( + bContext *C, const int mode, int totpoints, + const bool clear_point, const bool clear_stroke, + tGPencilPointCache *src_array) +{ + bGPDspoint *pt; + int i; + int idx = 0; + + /* create selected point array an fill it */ + bGPDstroke **gps_array = MEM_callocN(sizeof(bGPDstroke *) * totpoints, __func__); + bGPDspoint *pt_array = MEM_callocN(sizeof(bGPDspoint) * totpoints, __func__); + + CTX_DATA_BEGIN(C, bGPDlayer *, gpl, editable_gpencil_layers) + { + bGPDframe *gpf = gpl->actframe; + if (gpf == NULL) { + continue; + } + for (bGPDstroke *gps = gpf->strokes.first; gps; gps = gps->next) { + if (gps->flag & GP_STROKE_SELECT) { + for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) { + if (clear_stroke) { + pt->flag |= GP_SPOINT_TAG; + } + else { + pt->flag &= ~GP_SPOINT_TAG; + } + + if (pt->flag & GP_SPOINT_SELECT) { + bGPDspoint *pt2 = &pt_array[idx]; + copy_v3_v3(&pt2->x, &pt->x); + pt2->pressure = pt->pressure; + pt2->strength = pt->strength; + pt->flag &= ~GP_SPOINT_SELECT; + if (clear_point) { + pt->flag |= GP_SPOINT_TAG; + } + + /* save stroke */ + gps_array[idx] = gps; + + idx++; + } + } + gps->flag &= ~GP_STROKE_SELECT; + } + } + } + CTX_DATA_END; + + /* project in 2d plane */ + int direction = 0; + float(*points2d)[2] = MEM_mallocN(sizeof(*points2d) * totpoints, "GP Stroke temp 2d points"); + BKE_gpencil_stroke_2d_flat(pt_array, totpoints, points2d, &direction); + + /* calc center */ + float center[2] = { 0.0f, 0.0f }; + for (i = 0; i < totpoints; i++) { + center[0] += points2d[i][0]; + center[1] += points2d[i][1]; + } + mul_v2_fl(center, 1.0f / totpoints); + + /* calc angle and distance to center for each point */ + const float axis[2] = { 1.0f, 0.0f }; + float v1[3]; + for (i = 0; i < totpoints; i++) { + float ln = len_v2v2(center, points2d[i]); + sub_v2_v2v2(v1, points2d[i], center); + float angle = angle_signed_v2v2(axis, v1); + if (angle < 0.0f) { + angle = fabsf(angle); + } + else { + angle = (M_PI * 2.0) - angle; + } + tGPencilPointCache *sort_pt = &src_array[i]; + bGPDspoint *pt2 = &pt_array[i]; + + copy_v3_v3(&sort_pt->x, &pt2->x); + sort_pt->pressure = pt2->pressure; + sort_pt->strength = pt2->strength; + + sort_pt->gps = gps_array[i]; + + if (mode == GP_MERGE_STROKE) { + sort_pt->factor = angle; + } + else { + sort_pt->factor = (angle * 100000.0f) + ln; + } + } + MEM_SAFE_FREE(points2d); + MEM_SAFE_FREE(gps_array); + MEM_SAFE_FREE(pt_array); +} + +/* insert a group of points in destination array */ +static int gpencil_insert_to_array( + tGPencilPointCache *src_array, tGPencilPointCache *dst_array, int totpoints, + bGPDstroke *gps_filter, bool reverse, int last) +{ + tGPencilPointCache *src_elem = NULL; + tGPencilPointCache *dst_elem = NULL; + int idx = 0; + + for (int i = 0; i < totpoints; i++) { + if (!reverse) { + idx = i; + } + else { + idx = totpoints - i - 1; + } + src_elem = &src_array[idx]; + /* check if all points or only a stroke */ + if ((gps_filter != NULL) && (gps_filter != src_elem->gps)) { + continue; + } + + dst_elem = &dst_array[last]; + last++; + + copy_v3_v3(&dst_elem->x, &src_elem->x); + dst_elem->gps = src_elem->gps; + dst_elem->pressure = src_elem->pressure; + dst_elem->strength = src_elem->strength; + dst_elem->factor = src_elem->factor; + } + + return last; +} + +/* get first and last point location */ +static void gpencil_get_extremes( + tGPencilPointCache *src_array, int totpoints, + bGPDstroke *gps_filter, float *start, float *end) +{ + tGPencilPointCache *array_pt = NULL; + int i; + + /* find first point */ + for (i = 0; i < totpoints; i++) { + array_pt = &src_array[i]; + if (gps_filter == array_pt->gps) { + copy_v3_v3(start, &array_pt->x); + break; + } + } + /* find last point */ + for (i = totpoints - 1; i >= 0; i--) { + array_pt = &src_array[i]; + if (gps_filter == array_pt->gps) { + copy_v3_v3(end, &array_pt->x); + break; + } + } +} + +static int gpencil_analyze_strokes( + tGPencilPointCache *src_array, int totstrokes, int totpoints, + tGPencilPointCache *dst_array) +{ + int i; + int last = 0; + GHash *all_strokes = BLI_ghash_ptr_new(__func__); + /* add first stroke to array */ + tGPencilPointCache *sort_pt = &src_array[0]; + bGPDstroke *gps = sort_pt->gps; + last = gpencil_insert_to_array(src_array, dst_array, totpoints, gps, false, last); + float start[3]; + float end[3]; + float end_prv[3]; + gpencil_get_extremes(src_array, totpoints, gps, start, end); + copy_v3_v3(end_prv, end); + BLI_ghash_insert(all_strokes, sort_pt->gps, sort_pt->gps); + + /* look for near stroke */ + bool loop = (bool)(totstrokes > 1); + while (loop) { + bGPDstroke *gps_next = NULL; + GHash *strokes = BLI_ghash_ptr_new(__func__); + float dist_start = 0.0f; + float dist_end = 0.0f; + float dist = FLT_MAX; + bool reverse = false; + + for (i = 0; i < totpoints; i++) { + sort_pt = &src_array[i]; + /* avoid dups */ + if (BLI_ghash_haskey(all_strokes, sort_pt->gps)) { + continue; + } + if (!BLI_ghash_haskey(strokes, sort_pt->gps)) { + gpencil_get_extremes(src_array, totpoints, sort_pt->gps, start, end); + /* distances to previous end */ + dist_start = len_v3v3(end_prv, start); + dist_end = len_v3v3(end_prv, end); + + if (dist > dist_start) { + gps_next = sort_pt->gps; + dist = dist_start; + reverse = false; + } + if (dist > dist_end) { + gps_next = sort_pt->gps; + dist = dist_end; + reverse = true; + } + BLI_ghash_insert(strokes, sort_pt->gps, sort_pt->gps); + } + } + BLI_ghash_free(strokes, NULL, NULL); + + /* add the stroke to array */ + if (gps->next != NULL) { + BLI_ghash_insert(all_strokes, gps_next, gps_next); + last = gpencil_insert_to_array(src_array, dst_array, totpoints, gps_next, reverse, last); + /* replace last end */ + sort_pt = &dst_array[last - 1]; + copy_v3_v3(end_prv, &sort_pt->x); + } + + /* loop exit */ + if (last >= totpoints) { + loop = false; + } + } + + BLI_ghash_free(all_strokes, NULL, NULL); + return last; +} + +static bool gp_strokes_merge_poll(bContext *C) +{ + /* only supported with grease pencil objects */ + Object *ob = CTX_data_active_object(C); + if ((ob == NULL) || (ob->type != OB_GPENCIL)) { + return false; + } + + /* check material */ + Material *ma = NULL; + ma = give_current_material(ob, ob->actcol); + if ((ma == NULL) || (ma->gp_style == NULL)) { + return false; + } + + /* check hidden or locked materials */ + MaterialGPencilStyle *gp_style = ma->gp_style; + if ((gp_style->flag & GP_STYLE_COLOR_HIDE) || + (gp_style->flag & GP_STYLE_COLOR_LOCKED)) + { + return false; + } + + /* check layer */ + bGPDlayer *gpl = CTX_data_active_gpencil_layer(C); + if ((gpl == NULL) || + (gpl->flag & GP_LAYER_LOCKED) || + (gpl->flag & GP_LAYER_HIDE)) + { + return false; + } + + /* NOTE: this is a bit slower, but is the most accurate... */ + return (CTX_DATA_COUNT(C, editable_gpencil_strokes) != 0) && ED_operator_view3d_active(C); +} + +static int gp_stroke_merge_exec(bContext *C, wmOperator *op) +{ + const int mode = RNA_enum_get(op->ptr, "mode"); + const bool clear_point = RNA_boolean_get(op->ptr, "clear_point"); + const bool clear_stroke = RNA_boolean_get(op->ptr, "clear_stroke"); + + Object *ob = CTX_data_active_object(C); + /* sanity checks */ + if (!ob || ob->type != OB_GPENCIL) { + return OPERATOR_CANCELLED; + } + + bGPdata *gpd = (bGPdata *)ob->data; + bGPDlayer *gpl = CTX_data_active_gpencil_layer(C); + if (gpl == NULL) { + return OPERATOR_CANCELLED; + } + + int totstrokes = 0; + int totpoints = 0; + + /* count number of strokes and selected points */ + gpencil_get_elements_len(C, &totstrokes, &totpoints); + + if (totpoints == 0) { + return OPERATOR_CANCELLED; + } + + /* calc factor of each point and fill an array with all data */ + tGPencilPointCache *sorted_array = NULL; + tGPencilPointCache *original_array = MEM_callocN(sizeof(tGPencilPointCache) * totpoints, __func__); + gpencil_calc_points_factor(C, mode, totpoints, clear_point, clear_stroke, original_array); + + /* for strokes analyze strokes and load sorted array */ + if (mode == GP_MERGE_STROKE) { + sorted_array = MEM_callocN(sizeof(tGPencilPointCache) * totpoints, __func__); + totpoints = gpencil_analyze_strokes(original_array, totstrokes, totpoints, sorted_array); + } + else { + /* make a copy to sort */ + sorted_array = MEM_dupallocN(original_array); + /* sort by factor around center */ + qsort(sorted_array, totpoints, sizeof(tGPencilPointCache), gpencil_sort_points); + } + + /* prepare the new stroke */ + bGPDstroke *gps = gpencil_prepare_stroke(C, op, totpoints); + + /* copy original points to final stroke */ + gpencil_insert_points_to_stroke(gps, sorted_array, totpoints); + + /* dissolve all tagged points */ + if ((clear_point) || (clear_stroke)) { + gpencil_dissolve_points(C); + } + + /* free memory */ + MEM_SAFE_FREE(original_array); + MEM_SAFE_FREE(sorted_array); + + /* notifiers */ + DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL); + + return OPERATOR_FINISHED; +} + +void GPENCIL_OT_stroke_merge(wmOperatorType *ot) +{ + static const EnumPropertyItem mode_type[] = { + {GP_MERGE_STROKE, "STROKE", 0, "Stroke", ""}, + {GP_MERGE_POINT, "POINT", 0, "Point", ""}, + {0, NULL, 0, NULL, NULL} + }; + + /* identifiers */ + ot->name = "Merge Strokes"; + ot->idname = "GPENCIL_OT_stroke_merge"; + ot->description = "Create a new stroke with the selected stroke points"; + + /* api callbacks */ + ot->exec = gp_stroke_merge_exec; + ot->poll = gp_strokes_merge_poll; + + /* flags */ + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + /* properties */ + ot->prop = RNA_def_enum(ot->srna, "mode", mode_type, GP_MERGE_STROKE, "Mode", ""); + RNA_def_boolean(ot->srna, "back", 0, "Draw on Back", "Draw new stroke below all previous strokes"); + RNA_def_boolean(ot->srna, "additive", 0, "Additive Drawing", "Add to previous drawing"); + RNA_def_boolean(ot->srna, "cyclic", 0, "Cyclic", "Close new stroke"); + RNA_def_boolean(ot->srna, "clear_point", 0, "Dissolve Points", "Dissolve old selected points"); + RNA_def_boolean(ot->srna, "clear_stroke", 0, "Delete Strokes", "Delete old selected strokes"); +} diff --git a/source/blender/editors/gpencil/gpencil_ops.c b/source/blender/editors/gpencil/gpencil_ops.c index 3eaf72121a8..7814aa963ba 100644 --- a/source/blender/editors/gpencil/gpencil_ops.c +++ b/source/blender/editors/gpencil/gpencil_ops.c @@ -309,6 +309,7 @@ void ED_operatortypes_gpencil(void) WM_operatortype_append(GPENCIL_OT_stroke_separate); WM_operatortype_append(GPENCIL_OT_stroke_split); WM_operatortype_append(GPENCIL_OT_stroke_smooth); + WM_operatortype_append(GPENCIL_OT_stroke_merge); WM_operatortype_append(GPENCIL_OT_brush_presets_create); |