diff options
Diffstat (limited to 'source/blender/blenkernel/intern/gpencil_geom.c')
-rw-r--r-- | source/blender/blenkernel/intern/gpencil_geom.c | 584 |
1 files changed, 584 insertions, 0 deletions
diff --git a/source/blender/blenkernel/intern/gpencil_geom.c b/source/blender/blenkernel/intern/gpencil_geom.c index d2cfb36cb15..fc74439fd76 100644 --- a/source/blender/blenkernel/intern/gpencil_geom.c +++ b/source/blender/blenkernel/intern/gpencil_geom.c @@ -1345,6 +1345,34 @@ float BKE_gpencil_stroke_length(const bGPDstroke *gps, bool use_3d) return total_length; } +/** Calculate grease pencil stroke length between points. */ +float BKE_gpencil_stroke_segment_length(const struct bGPDstroke *gps, + const int start_index, + const int end_index, + bool use_3d) +{ + if (!gps->points || gps->totpoints < 2 || end_index <= start_index) { + return 0.0f; + } + + int index = MAX2(start_index, 0) + 1; + int last_index = MIN2(end_index, gps->totpoints - 1) + 1; + + float *last_pt = &gps->points[index - 1].x; + float total_length = 0.0f; + for (int i = index; i < last_index; i++) { + bGPDspoint *pt = &gps->points[i]; + if (use_3d) { + total_length += len_v3v3(&pt->x, last_pt); + } + else { + total_length += len_v2v2(&pt->x, last_pt); + } + last_pt = &pt->x; + } + return total_length; +} + /** * Trim stroke to the first intersection or loop. * \param gps: Stroke data @@ -2640,4 +2668,560 @@ void BKE_gpencil_stroke_set_random_color(bGPDstroke *gps) copy_v4_v4(pt->vert_color, color); } } + +/* Flip stroke. */ +void BKE_gpencil_stroke_flip(bGPDstroke *gps) +{ + int end = gps->totpoints - 1; + + for (int i = 0; i < gps->totpoints / 2; i++) { + bGPDspoint *point, *point2; + bGPDspoint pt; + + /* save first point */ + point = &gps->points[i]; + pt.x = point->x; + pt.y = point->y; + pt.z = point->z; + pt.flag = point->flag; + pt.pressure = point->pressure; + pt.strength = point->strength; + pt.time = point->time; + copy_v4_v4(pt.vert_color, point->vert_color); + + /* replace first point with last point */ + point2 = &gps->points[end]; + point->x = point2->x; + point->y = point2->y; + point->z = point2->z; + point->flag = point2->flag; + point->pressure = point2->pressure; + point->strength = point2->strength; + point->time = point2->time; + copy_v4_v4(point->vert_color, point2->vert_color); + + /* replace last point with first saved before */ + point = &gps->points[end]; + point->x = pt.x; + point->y = pt.y; + point->z = pt.z; + point->flag = pt.flag; + point->pressure = pt.pressure; + point->strength = pt.strength; + point->time = pt.time; + copy_v4_v4(point->vert_color, pt.vert_color); + + end--; + } +} + +/* Temp data for storing information about an "island" of points + * that should be kept when splitting up a stroke. Used in: + * gpencil_stroke_delete_tagged_points() + */ +typedef struct tGPDeleteIsland { + int start_idx; + int end_idx; +} tGPDeleteIsland; + +static void gpencil_stroke_join_islands(bGPdata *gpd, + bGPDframe *gpf, + bGPDstroke *gps_first, + bGPDstroke *gps_last) +{ + bGPDspoint *pt = NULL; + bGPDspoint *pt_final = NULL; + const int totpoints = gps_first->totpoints + gps_last->totpoints; + + /* create new stroke */ + bGPDstroke *join_stroke = BKE_gpencil_stroke_duplicate(gps_first, false, true); + + join_stroke->points = MEM_callocN(sizeof(bGPDspoint) * totpoints, __func__); + join_stroke->totpoints = totpoints; + join_stroke->flag &= ~GP_STROKE_CYCLIC; + + /* copy points (last before) */ + int e1 = 0; + int e2 = 0; + float delta = 0.0f; + + for (int i = 0; i < totpoints; i++) { + pt_final = &join_stroke->points[i]; + if (i < gps_last->totpoints) { + pt = &gps_last->points[e1]; + e1++; + } + else { + pt = &gps_first->points[e2]; + e2++; + } + + /* copy current point */ + copy_v3_v3(&pt_final->x, &pt->x); + pt_final->pressure = pt->pressure; + pt_final->strength = pt->strength; + pt_final->time = delta; + pt_final->flag = pt->flag; + copy_v4_v4(pt_final->vert_color, pt->vert_color); + + /* retiming with fixed time interval (we cannot determine real time) */ + delta += 0.01f; + } + + /* Copy over vertex weight data (if available) */ + if ((gps_first->dvert != NULL) || (gps_last->dvert != NULL)) { + join_stroke->dvert = MEM_callocN(sizeof(MDeformVert) * totpoints, __func__); + MDeformVert *dvert_src = NULL; + MDeformVert *dvert_dst = NULL; + + /* Copy weights (last before)*/ + e1 = 0; + e2 = 0; + for (int i = 0; i < totpoints; i++) { + dvert_dst = &join_stroke->dvert[i]; + dvert_src = NULL; + if (i < gps_last->totpoints) { + if (gps_last->dvert) { + dvert_src = &gps_last->dvert[e1]; + e1++; + } + } + else { + if (gps_first->dvert) { + dvert_src = &gps_first->dvert[e2]; + e2++; + } + } + + if ((dvert_src) && (dvert_src->dw)) { + dvert_dst->dw = MEM_dupallocN(dvert_src->dw); + } + } + } + + /* add new stroke at head */ + BLI_addhead(&gpf->strokes, join_stroke); + /* Calc geometry data. */ + BKE_gpencil_stroke_geometry_update(gpd, join_stroke); + + /* remove first stroke */ + BLI_remlink(&gpf->strokes, gps_first); + BKE_gpencil_free_stroke(gps_first); + + /* remove last stroke */ + BLI_remlink(&gpf->strokes, gps_last); + BKE_gpencil_free_stroke(gps_last); +} + +/* Split the given stroke into several new strokes, partitioning + * it based on whether the stroke points have a particular flag + * is set (e.g. "GP_SPOINT_SELECT" in most cases, but not always) + * + * The algorithm used here is as follows: + * 1) We firstly identify the number of "islands" of non-tagged points + * which will all end up being in new strokes. + * - In the most extreme case (i.e. every other vert is a 1-vert island), + * we have at most n / 2 islands + * - Once we start having larger islands than that, the number required + * becomes much less + * 2) Each island gets converted to a new stroke + * If the number of points is <= limit, the stroke is deleted + */ +bGPDstroke *BKE_gpencil_stroke_delete_tagged_points(bGPdata *gpd, + bGPDframe *gpf, + bGPDstroke *gps, + bGPDstroke *next_stroke, + int tag_flags, + bool select, + int limit) +{ + tGPDeleteIsland *islands = MEM_callocN(sizeof(tGPDeleteIsland) * (gps->totpoints + 1) / 2, + "gp_point_islands"); + bool in_island = false; + int num_islands = 0; + + bGPDstroke *new_stroke = NULL; + bGPDstroke *gps_first = NULL; + const bool is_cyclic = (bool)(gps->flag & GP_STROKE_CYCLIC); + + /* First Pass: Identify start/end of islands */ + bGPDspoint *pt = gps->points; + for (int i = 0; i < gps->totpoints; i++, pt++) { + if (pt->flag & tag_flags) { + /* selected - stop accumulating to island */ + in_island = false; + } + else { + /* unselected - start of a new island? */ + int idx; + + if (in_island) { + /* extend existing island */ + idx = num_islands - 1; + islands[idx].end_idx = i; + } + else { + /* start of new island */ + in_island = true; + num_islands++; + + idx = num_islands - 1; + islands[idx].start_idx = islands[idx].end_idx = i; + } + } + } + + /* Watch out for special case where No islands = All points selected = Delete Stroke only */ + if (num_islands) { + /* There are islands, so create a series of new strokes, + * adding them before the "next" stroke. */ + int idx; + + /* Create each new stroke... */ + for (idx = 0; idx < num_islands; idx++) { + tGPDeleteIsland *island = &islands[idx]; + new_stroke = BKE_gpencil_stroke_duplicate(gps, false, true); + + /* if cyclic and first stroke, save to join later */ + if ((is_cyclic) && (gps_first == NULL)) { + gps_first = new_stroke; + } + + new_stroke->flag &= ~GP_STROKE_CYCLIC; + + /* Compute new buffer size (+ 1 needed as the endpoint index is "inclusive") */ + new_stroke->totpoints = island->end_idx - island->start_idx + 1; + + /* Copy over the relevant point data */ + new_stroke->points = MEM_callocN(sizeof(bGPDspoint) * new_stroke->totpoints, + "gp delete stroke fragment"); + memcpy(new_stroke->points, + gps->points + island->start_idx, + sizeof(bGPDspoint) * new_stroke->totpoints); + + /* Copy over vertex weight data (if available) */ + if (gps->dvert != NULL) { + /* Copy over the relevant vertex-weight points */ + new_stroke->dvert = MEM_callocN(sizeof(MDeformVert) * new_stroke->totpoints, + "gp delete stroke fragment weight"); + memcpy(new_stroke->dvert, + gps->dvert + island->start_idx, + sizeof(MDeformVert) * new_stroke->totpoints); + + /* Copy weights */ + int e = island->start_idx; + for (int i = 0; i < new_stroke->totpoints; i++) { + MDeformVert *dvert_src = &gps->dvert[e]; + MDeformVert *dvert_dst = &new_stroke->dvert[i]; + if (dvert_src->dw) { + dvert_dst->dw = MEM_dupallocN(dvert_src->dw); + } + e++; + } + } + /* Each island corresponds to a new stroke. + * We must adjust the timings of these new strokes: + * + * Each point's timing data is a delta from stroke's inittime, so as we erase some points + * from the start of the stroke, we have to offset this inittime and all remaining points' + * delta values. This way we get a new stroke with exactly the same timing as if user had + * started drawing from the first non-removed point. + */ + { + bGPDspoint *pts; + float delta = gps->points[island->start_idx].time; + int j; + + new_stroke->inittime += (double)delta; + + pts = new_stroke->points; + for (j = 0; j < new_stroke->totpoints; j++, pts++) { + pts->time -= delta; + /* set flag for select again later */ + if (select == true) { + pts->flag &= ~GP_SPOINT_SELECT; + pts->flag |= GP_SPOINT_TAG; + } + } + } + + /* Add new stroke to the frame or delete if below limit */ + if ((limit > 0) && (new_stroke->totpoints <= limit)) { + BKE_gpencil_free_stroke(new_stroke); + } + else { + /* Calc geometry data. */ + BKE_gpencil_stroke_geometry_update(gpd, new_stroke); + + if (next_stroke) { + BLI_insertlinkbefore(&gpf->strokes, next_stroke, new_stroke); + } + else { + BLI_addtail(&gpf->strokes, new_stroke); + } + } + } + /* if cyclic, need to join last stroke with first stroke */ + if ((is_cyclic) && (gps_first != NULL) && (gps_first != new_stroke)) { + gpencil_stroke_join_islands(gpd, gpf, gps_first, new_stroke); + } + } + + /* free islands */ + MEM_freeN(islands); + + /* Delete the old stroke */ + BLI_remlink(&gpf->strokes, gps); + BKE_gpencil_free_stroke(gps); + + return new_stroke; +} + +void BKE_gpencil_curve_delete_tagged_points(bGPdata *gpd, + bGPDframe *gpf, + bGPDstroke *gps, + bGPDstroke *next_stroke, + bGPDcurve *gpc, + int tag_flags) +{ + if (gpc == NULL) { + return; + } + const bool is_cyclic = gps->flag & GP_STROKE_CYCLIC; + const int idx_last = gpc->tot_curve_points - 1; + bGPDstroke *gps_first = NULL; + bGPDstroke *gps_last = NULL; + + int idx_start = 0; + int idx_end = 0; + bool prev_selected = gpc->curve_points[0].flag & tag_flags; + for (int i = 1; i < gpc->tot_curve_points; i++) { + bool selected = gpc->curve_points[i].flag & tag_flags; + if (prev_selected == true && selected == false) { + idx_start = i; + } + /* Island ends if the current point is selected or if we reached the end of the stroke */ + if ((prev_selected == false && selected == true) || (selected == false && i == idx_last)) { + + idx_end = selected ? i - 1 : i; + int island_length = idx_end - idx_start + 1; + + /* If an island has only a single curve point, there is no curve segment, so skip island */ + if (island_length == 1) { + if (is_cyclic) { + if (idx_start > 0 && idx_end < idx_last) { + prev_selected = selected; + continue; + } + } + else { + prev_selected = selected; + continue; + } + } + + bGPDstroke *new_stroke = BKE_gpencil_stroke_duplicate(gps, false, false); + new_stroke->points = NULL; + new_stroke->flag &= ~GP_STROKE_CYCLIC; + new_stroke->editcurve = BKE_gpencil_stroke_editcurve_new(island_length); + + if (gps_first == NULL) { + gps_first = new_stroke; + } + + bGPDcurve *new_gpc = new_stroke->editcurve; + memcpy(new_gpc->curve_points, + gpc->curve_points + idx_start, + sizeof(bGPDcurve_point) * island_length); + + BKE_gpencil_editcurve_recalculate_handles(new_stroke); + new_stroke->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; + + /* Calc geometry data. */ + BKE_gpencil_stroke_geometry_update(gpd, new_stroke); + + if (next_stroke) { + BLI_insertlinkbefore(&gpf->strokes, next_stroke, new_stroke); + } + else { + BLI_addtail(&gpf->strokes, new_stroke); + } + + gps_last = new_stroke; + } + prev_selected = selected; + } + + /* join first and last stroke if cyclic */ + if (is_cyclic && gps_first != NULL && gps_last != NULL && gps_first != gps_last) { + bGPDcurve *gpc_first = gps_first->editcurve; + bGPDcurve *gpc_last = gps_last->editcurve; + int first_tot_points = gpc_first->tot_curve_points; + int old_tot_points = gpc_last->tot_curve_points; + + gpc_last->tot_curve_points = first_tot_points + old_tot_points; + gpc_last->curve_points = MEM_recallocN(gpc_last->curve_points, + sizeof(bGPDcurve_point) * gpc_last->tot_curve_points); + /* copy data from first to last */ + memcpy(gpc_last->curve_points + old_tot_points, + gpc_first->curve_points, + sizeof(bGPDcurve_point) * first_tot_points); + + BKE_gpencil_editcurve_recalculate_handles(gps_last); + gps_last->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; + + /* Calc geometry data. */ + BKE_gpencil_stroke_geometry_update(gpd, gps_last); + + /* remove first one */ + BLI_remlink(&gpf->strokes, gps_first); + BKE_gpencil_free_stroke(gps_first); + } + + /* Delete the old stroke */ + BLI_remlink(&gpf->strokes, gps); + BKE_gpencil_free_stroke(gps); +} + +/* Helper: copy point between strokes */ +static void gpencil_stroke_copy_point(bGPDstroke *gps, + MDeformVert *dvert, + bGPDspoint *point, + const float delta[3], + float pressure, + float strength, + float deltatime) +{ + bGPDspoint *newpoint; + + gps->points = MEM_reallocN(gps->points, sizeof(bGPDspoint) * (gps->totpoints + 1)); + if (gps->dvert != NULL) { + gps->dvert = MEM_reallocN(gps->dvert, sizeof(MDeformVert) * (gps->totpoints + 1)); + } + else { + /* If destination has weight add weight to origin. */ + if (dvert != NULL) { + gps->dvert = MEM_callocN(sizeof(MDeformVert) * (gps->totpoints + 1), __func__); + } + } + + gps->totpoints++; + newpoint = &gps->points[gps->totpoints - 1]; + + newpoint->x = point->x * delta[0]; + newpoint->y = point->y * delta[1]; + newpoint->z = point->z * delta[2]; + newpoint->flag = point->flag; + newpoint->pressure = pressure; + newpoint->strength = strength; + newpoint->time = point->time + deltatime; + copy_v4_v4(newpoint->vert_color, point->vert_color); + + if (gps->dvert != NULL) { + MDeformVert *newdvert = &gps->dvert[gps->totpoints - 1]; + + if (dvert != NULL) { + newdvert->totweight = dvert->totweight; + newdvert->dw = MEM_dupallocN(dvert->dw); + } + else { + newdvert->totweight = 0; + newdvert->dw = NULL; + } + } +} + +/* Join two strokes using the shortest distance (reorder stroke if necessary ) */ +void BKE_gpencil_stroke_join(bGPDstroke *gps_a, + bGPDstroke *gps_b, + const bool leave_gaps, + const bool fit_thickness) +{ + bGPDspoint point; + bGPDspoint *pt; + int i; + const float delta[3] = {1.0f, 1.0f, 1.0f}; + float deltatime = 0.0f; + + /* sanity checks */ + if (ELEM(NULL, gps_a, gps_b)) { + return; + } + + if ((gps_a->totpoints == 0) || (gps_b->totpoints == 0)) { + return; + } + + /* define start and end points of each stroke */ + float start_a[3], start_b[3], end_a[3], end_b[3]; + pt = &gps_a->points[0]; + copy_v3_v3(start_a, &pt->x); + + pt = &gps_a->points[gps_a->totpoints - 1]; + copy_v3_v3(end_a, &pt->x); + + pt = &gps_b->points[0]; + copy_v3_v3(start_b, &pt->x); + + pt = &gps_b->points[gps_b->totpoints - 1]; + copy_v3_v3(end_b, &pt->x); + + /* Check if need flip strokes. */ + float dist = len_squared_v3v3(end_a, start_b); + bool flip_a = false; + bool flip_b = false; + float lowest = dist; + + dist = len_squared_v3v3(end_a, end_b); + if (dist < lowest) { + lowest = dist; + flip_a = false; + flip_b = true; + } + + dist = len_squared_v3v3(start_a, start_b); + if (dist < lowest) { + lowest = dist; + flip_a = true; + flip_b = false; + } + + dist = len_squared_v3v3(start_a, end_b); + if (dist < lowest) { + lowest = dist; + flip_a = true; + flip_b = true; + } + + if (flip_a) { + BKE_gpencil_stroke_flip(gps_a); + } + if (flip_b) { + BKE_gpencil_stroke_flip(gps_b); + } + + /* don't visibly link the first and last points? */ + if (leave_gaps) { + /* 1st: add one tail point to start invisible area */ + point = gps_a->points[gps_a->totpoints - 1]; + deltatime = point.time; + + gpencil_stroke_copy_point(gps_a, NULL, &point, delta, 0.0f, 0.0f, 0.0f); + + /* 2nd: add one head point to finish invisible area */ + point = gps_b->points[0]; + gpencil_stroke_copy_point(gps_a, NULL, &point, delta, 0.0f, 0.0f, deltatime); + } + + const float ratio = (fit_thickness && gps_a->thickness > 0.0f) ? + (float)gps_b->thickness / (float)gps_a->thickness : + 1.0f; + + /* 3rd: add all points */ + for (i = 0, pt = gps_b->points; i < gps_b->totpoints && pt; i++, pt++) { + MDeformVert *dvert = (gps_b->dvert) ? &gps_b->dvert[i] : NULL; + gpencil_stroke_copy_point( + gps_a, dvert, pt, delta, pt->pressure * ratio, pt->strength, deltatime); + } +} /** \} */ |