GPencil: Automerge last drawn stroke with previous strokes

This option joins any stroke with an end near  the actual stroke. Now it is not limited to the last stroke, any stroke in the same layer for the actual frame can be joined. The join can join two strokes drawing a third stroke.

If the end and the start of the result stroke are very small, the stroke is changed to be cyclic automatically.

There is a limit distance to join the stroke, if the distance is greater than this value, the strokes are not joined. Actually, a constant, threshold distance is used, but we could expose 
as a parameter in the UI in the future.

The tool can be used with freehand drawing or with primitives.

Note: Great part of the patch is just a refactor of the old code to make it accessible and to keep code organized.

Reviewed By: mendio

Maniphest Tasks: T82377

Differential Revision: https://developer.blender.org/D9440
This commit is contained in:
Antonio Vazquez 2020-11-18 21:30:43 +01:00 committed by Antonio Vazquez
parent c126e27cdc
commit e9607f45d8
Notes: blender-bot 2023-02-14 10:21:10 +01:00
Referenced by commit 5df916f23f, Fix T94366: Grease Pencil Automerge no immediate UI update
Referenced by issue #82377, GPencil: New autojoin strokes
14 changed files with 910 additions and 597 deletions

View File

@ -657,6 +657,8 @@ class VIEW3D_HT_header(Header):
sub.prop(tool_settings, "use_gpencil_weight_data_add", text="", icon='WPAINT_HLT')
sub.separator(factor=0.4)
sub.prop(tool_settings, "use_gpencil_draw_additive", text="", icon='FREEZE')
sub.separator(factor=0.4)
sub.prop(tool_settings, "use_gpencil_automerge_strokes", text="")
# Select mode for Editing
if gpd.use_stroke_edit_mode:

View File

@ -37,6 +37,7 @@ struct bGPDlayer;
struct bGPDspoint;
struct bGPDstroke;
struct bGPdata;
struct bGPDcurve;
/* Object boundbox. */
bool BKE_gpencil_data_minmax(const struct bGPdata *gpd, float r_min[3], float r_max[3]);
@ -115,6 +116,21 @@ bool BKE_gpencil_stroke_stretch(struct bGPDstroke *gps, const float dist, const
bool BKE_gpencil_stroke_trim_points(struct bGPDstroke *gps,
const int index_from,
const int index_to);
struct bGPDstroke *BKE_gpencil_stroke_delete_tagged_points(struct bGPdata *gpd,
struct bGPDframe *gpf,
struct bGPDstroke *gps,
struct bGPDstroke *next_stroke,
int tag_flags,
bool select,
int limit);
void BKE_gpencil_curve_delete_tagged_points(struct bGPdata *gpd,
struct bGPDframe *gpf,
struct bGPDstroke *gps,
struct bGPDstroke *next_stroke,
struct bGPDcurve *gpc,
int tag_flags);
void BKE_gpencil_stroke_flip(struct bGPDstroke *gps);
bool BKE_gpencil_stroke_split(struct bGPdata *gpd,
struct bGPDframe *gpf,
struct bGPDstroke *gps,
@ -123,9 +139,18 @@ bool BKE_gpencil_stroke_split(struct bGPdata *gpd,
bool BKE_gpencil_stroke_shrink(struct bGPDstroke *gps, const float dist);
float BKE_gpencil_stroke_length(const struct bGPDstroke *gps, bool use_3d);
float BKE_gpencil_stroke_segment_length(const struct bGPDstroke *gps,
const int start_index,
const int end_index,
bool use_3d);
void BKE_gpencil_stroke_set_random_color(struct bGPDstroke *gps);
void BKE_gpencil_stroke_join(struct bGPDstroke *gps_a,
struct bGPDstroke *gps_b,
const bool leave_gaps,
const bool fit_thickness);
bool BKE_gpencil_convert_mesh(struct Main *bmain,
struct Depsgraph *depsgraph,
struct Scene *scene,

View File

@ -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);
}
}
/** \} */

View File

@ -42,6 +42,7 @@
#include "BKE_context.h"
#include "BKE_global.h"
#include "BKE_gpencil.h"
#include "BKE_gpencil_geom.h"
#include "BKE_layer.h"
#include "BKE_main.h"
#include "BKE_report.h"
@ -1198,7 +1199,8 @@ static void annotation_stroke_eraser_dostroke(tGPsdata *p,
/* Second Pass: Remove any points that are tagged */
if (do_cull) {
gpencil_stroke_delete_tagged_points(p->gpd, gpf, gps, gps->next, GP_SPOINT_TAG, false, 0);
BKE_gpencil_stroke_delete_tagged_points(
p->gpd, gpf, gps, gps->next, GP_SPOINT_TAG, false, 0);
}
}
}

View File

@ -1860,7 +1860,8 @@ static int image_to_gpencil_exec(bContext *C, wmOperator *op)
if (done) {
/* Delete any selected point. */
LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) {
gpencil_stroke_delete_tagged_points(gpd, gpf, gps, gps->next, GP_SPOINT_SELECT, false, 0);
BKE_gpencil_stroke_delete_tagged_points(
gpd, gpf, gps, gps->next, GP_SPOINT_SELECT, false, 0);
}
BKE_reportf(op->reports, RPT_INFO, "Object created");

View File

@ -99,8 +99,6 @@
/** \name Stroke Edit Mode Management
* \{ */
static void gpencil_flip_stroke(bGPDstroke *gps);
/* poll callback for all stroke editing operators */
static bool gpencil_stroke_edit_poll(bContext *C)
{
@ -1181,7 +1179,7 @@ static void gpencil_add_move_points(bGPdata *gpd, bGPDframe *gpf, bGPDstroke *gp
/* Flip stroke if it was only one point to consider extrude point as last point. */
if (gps->totpoints == 2) {
gpencil_flip_stroke(gps);
BKE_gpencil_stroke_flip(gps);
}
/* Calc geometry data. */
@ -2584,372 +2582,6 @@ static int gpencil_dissolve_selected_points(bContext *C, eGP_DissolveMode mode)
/* ----------------------------------- */
/* 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
*/
void 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 *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;
bGPDstroke *new_stroke = NULL;
/* 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);
}
static void 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);
}
/* Split selected strokes into segments, splitting on selected points */
static int gpencil_delete_selected_points(bContext *C)
{
@ -2987,12 +2619,12 @@ static int gpencil_delete_selected_points(bContext *C)
if (is_curve_edit) {
bGPDcurve *gpc = gps->editcurve;
gpencil_curve_delete_tagged_points(
BKE_gpencil_curve_delete_tagged_points(
gpd, gpf, gps, gps->next, gpc, GP_CURVE_POINT_SELECT);
}
else {
/* delete unwanted points by splitting stroke into several smaller ones */
gpencil_stroke_delete_tagged_points(
BKE_gpencil_stroke_delete_tagged_points(
gpd, gpf, gps, gps->next, GP_SPOINT_SELECT, false, 0);
}
@ -3828,188 +3460,6 @@ void GPENCIL_OT_stroke_caps_set(wmOperatorType *ot)
/** \name Stroke Join Operator
* \{ */
/* Helper: flip stroke */
static void gpencil_flip_stroke(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--;
}
}
/* 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;
}
}
}
/* Helper: join two strokes using the shortest distance (reorder stroke if necessary ) */
static void gpencil_stroke_join_strokes(bGPDstroke *gps_a,
bGPDstroke *gps_b,
const bool leave_gaps)
{
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) {
gpencil_flip_stroke(gps_a);
}
if (flip_b) {
gpencil_flip_stroke(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);
}
/* 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, pt->strength, deltatime);
}
}
typedef struct tJoinStrokes {
bGPDframe *gpf;
bGPDstroke *gps;
@ -4159,7 +3609,7 @@ static int gpencil_stroke_join_exec(bContext *C, wmOperator *op)
}
elem = &strokes_list[i];
/* Join new_stroke and stroke B. */
gpencil_stroke_join_strokes(gps_new, elem->gps, leave_gaps);
BKE_gpencil_stroke_join(gps_new, elem->gps, leave_gaps, true);
elem->used = true;
}
@ -4254,8 +3704,8 @@ static int gpencil_stroke_flip_exec(bContext *C, wmOperator *op)
BKE_report(op->reports, RPT_ERROR, "Not implemented!");
}
else {
/* flip stroke */
gpencil_flip_stroke(gps);
/* Flip stroke. */
BKE_gpencil_stroke_flip(gps);
}
changed = true;
@ -5108,11 +4558,11 @@ static int gpencil_stroke_separate_exec(bContext *C, wmOperator *op)
}
/* delete selected points from destination stroke */
gpencil_stroke_delete_tagged_points(
BKE_gpencil_stroke_delete_tagged_points(
gpd_dst, gpf_dst, gps_dst, NULL, GP_SPOINT_SELECT, false, 0);
/* delete selected points from origin stroke */
gpencil_stroke_delete_tagged_points(
BKE_gpencil_stroke_delete_tagged_points(
gpd_src, gpf, gps, gps->next, GP_SPOINT_SELECT, false, 0);
}
}
@ -5287,11 +4737,11 @@ static int gpencil_stroke_split_exec(bContext *C, wmOperator *op)
}
/* delete selected points from destination stroke */
gpencil_stroke_delete_tagged_points(
BKE_gpencil_stroke_delete_tagged_points(
gpd, gpf, gps_dst, NULL, GP_SPOINT_SELECT, true, 0);
/* delete selected points from origin stroke */
gpencil_stroke_delete_tagged_points(
BKE_gpencil_stroke_delete_tagged_points(
gpd, gpf, gps, gps->next, GP_SPOINT_SELECT, false, 0);
}
}
@ -5487,7 +4937,7 @@ static void gpencil_cutter_dissolve(bGPdata *gpd,
}
}
gpencil_stroke_delete_tagged_points(
BKE_gpencil_stroke_delete_tagged_points(
gpd, hit_layer->actframe, hit_stroke, gpsn, GP_SPOINT_TAG, false, 1);
}
}

View File

@ -152,6 +152,31 @@ typedef struct tGPDinterpolate {
NumInput num; /* numeric input */
} tGPDinterpolate;
/* Modal Operator Drawing Callbacks ------------------------ */
void ED_gpencil_draw_fill(struct tGPDdraw *tgpw);
/* ***************************************************** */
/* Internal API */
/* Stroke Coordinates API ------------------------------ */
/* gpencil_utils.c */
typedef struct GP_SpaceConversion {
struct Scene *scene;
struct Object *ob;
struct bGPdata *gpd;
struct bGPDlayer *gpl;
struct ScrArea *area;
struct ARegion *region;
struct View2D *v2d;
rctf *subrect; /* for using the camera rect within the 3d view */
rctf subrect_data;
float mat[4][4]; /* transform matrix on the strokes (introduced in [b770964]) */
} GP_SpaceConversion;
/* Temporary primitive operation data */
typedef struct tGPDprimitive {
/** main database pointer */
@ -180,6 +205,9 @@ typedef struct tGPDprimitive {
/** current brush */
struct Brush *brush;
/** Settings to pass to gp_points_to_xy(). */
GP_SpaceConversion gsc;
/** current frame number */
int cframe;
/** layer */
@ -248,31 +276,6 @@ typedef struct tGPDprimitive {
} tGPDprimitive;
/* Modal Operator Drawing Callbacks ------------------------ */
void ED_gpencil_draw_fill(struct tGPDdraw *tgpw);
/* ***************************************************** */
/* Internal API */
/* Stroke Coordinates API ------------------------------ */
/* gpencil_utils.c */
typedef struct GP_SpaceConversion {
struct Scene *scene;
struct Object *ob;
struct bGPdata *gpd;
struct bGPDlayer *gpl;
struct ScrArea *area;
struct ARegion *region;
struct View2D *v2d;
rctf *subrect; /* for using the camera rect within the 3d view */
rctf subrect_data;
float mat[4][4]; /* transform matrix on the strokes (introduced in [b770964]) */
} GP_SpaceConversion;
bool gpencil_stroke_inside_circle(const float mval[2], int rad, int x0, int y0, int x1, int y1);
void gpencil_point_conversion_init(struct bContext *C, GP_SpaceConversion *r_gsc);
@ -343,13 +346,6 @@ struct GHash *gpencil_copybuf_validate_colormap(struct bContext *C);
/* Stroke Editing ------------------------------------ */
void gpencil_stroke_delete_tagged_points(bGPdata *gpd,
bGPDframe *gpf,
bGPDstroke *gps,
bGPDstroke *next_stroke,
int tag_flags,
bool select,
int limit);
int gpencil_delete_selected_point_wrap(bContext *C);
void gpencil_subdivide_stroke(bGPdata *gpd, bGPDstroke *gps, const int subdivide);

View File

@ -182,7 +182,7 @@ static void gpencil_dissolve_points(bContext *C)
}
LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) {
gpencil_stroke_delete_tagged_points(gpd, gpf, gps, gps->next, GP_SPOINT_TAG, false, 0);
BKE_gpencil_stroke_delete_tagged_points(gpd, gpf, gps, gps->next, GP_SPOINT_TAG, false, 0);
}
}
CTX_DATA_END;

View File

@ -1282,6 +1282,25 @@ static void gpencil_stroke_newfrombuffer(tGPsdata *p)
BKE_gpencil_stroke_trim(gpd, gps);
}
/* Join with existing strokes. */
if (ts->gpencil_flags & GP_TOOL_FLAG_AUTOMERGE_STROKE) {
if (gps->prev != NULL) {
int pt_index = 0;
bool doit = true;
while (doit && gps) {
bGPDstroke *gps_target = ED_gpencil_stroke_nearest_to_ends(
p->C, &p->gsc, gpl, gpl->actframe, gps, GPENCIL_MINIMUM_JOIN_DIST, &pt_index);
if (gps_target != NULL) {
gps = ED_gpencil_stroke_join_and_trim(p->gpd, p->gpf, gps, gps_target, pt_index);
}
else {
doit = false;
}
}
}
ED_gpencil_stroke_close_by_distance(gps, 0.02f);
}
/* Calc geometry data. */
BKE_gpencil_stroke_geometry_update(gpd, gps);
@ -1652,7 +1671,8 @@ static void gpencil_stroke_eraser_dostroke(tGPsdata *p,
gpencil_stroke_soft_refine(gps);
}
gpencil_stroke_delete_tagged_points(p->gpd, gpf, gps, gps->next, GP_SPOINT_TAG, false, 0);
BKE_gpencil_stroke_delete_tagged_points(
p->gpd, gpf, gps, gps->next, GP_SPOINT_TAG, false, 0);
}
gpencil_update_cache(p->gpd);
}

View File

@ -1197,6 +1197,9 @@ static void gpencil_primitive_init(bContext *C, wmOperator *op)
/* set GP datablock */
tgpi->gpd = gpd;
/* Setup space conversions. */
gpencil_point_conversion_init(C, &tgpi->gsc);
/* if brush doesn't exist, create a new set (fix damaged files from old versions) */
if ((paint->brush == NULL) || (paint->brush->gpencil_settings == NULL)) {
BKE_brush_gpencil_paint_presets(bmain, ts, true);
@ -1347,6 +1350,28 @@ static void gpencil_primitive_interaction_end(bContext *C,
}
}
/* Join previous stroke. */
if (ts->gpencil_flags & GP_TOOL_FLAG_AUTOMERGE_STROKE) {
if (ELEM(tgpi->type, GP_STROKE_ARC, GP_STROKE_LINE, GP_STROKE_CURVE, GP_STROKE_POLYLINE)) {
if (gps->prev != NULL) {
int pt_index = 0;
bool doit = true;
while (doit && gps) {
bGPDstroke *gps_target = ED_gpencil_stroke_nearest_to_ends(
C, &tgpi->gsc, tgpi->gpl, gpf, gps, GPENCIL_MINIMUM_JOIN_DIST, &pt_index);
if (gps_target != NULL) {
gps = ED_gpencil_stroke_join_and_trim(tgpi->gpd, gpf, gps, gps_target, pt_index);
}
else {
doit = false;
}
}
}
ED_gpencil_stroke_close_by_distance(gps, 0.02f);
}
BKE_gpencil_stroke_geometry_update(tgpi->gpd, gps);
}
DEG_id_tag_update(&tgpi->gpd->id, ID_RECALC_COPY_ON_WRITE);
DEG_id_tag_update(&tgpi->gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);

View File

@ -3105,3 +3105,181 @@ bool ED_gpencil_stroke_point_is_inside(bGPDstroke *gps,
return hit;
}
bGPDstroke *ED_gpencil_stroke_nearest_to_ends(bContext *C,
GP_SpaceConversion *gsc,
bGPDlayer *gpl,
bGPDframe *gpf,
bGPDstroke *gps,
const float radius,
int *r_index)
{
Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
Object *ob = CTX_data_active_object(C);
bGPDstroke *gps_rtn = NULL;
const float radius_sqr = radius * radius;
/* calculate difference matrix object */
float diff_mat[4][4];
BKE_gpencil_parent_matrix_get(depsgraph, ob, gpl, diff_mat);
/* Calculate the extremes of the stroke in 2D. */
bGPDspoint pt_parent;
float pt2d_start[2], pt2d_end[2];
bGPDspoint *pt = &gps->points[0];
gpencil_point_to_parent_space(pt, diff_mat, &pt_parent);
gpencil_point_to_xy_fl(gsc, gps, &pt_parent, &pt2d_start[0], &pt2d_start[1]);
pt = &gps->points[gps->totpoints - 1];
gpencil_point_to_parent_space(pt, diff_mat, &pt_parent);
gpencil_point_to_xy_fl(gsc, gps, &pt_parent, &pt2d_end[0], &pt2d_end[1]);
/* Loop all strokes of the active frame. */
float dist_min = FLT_MAX;
LISTBASE_FOREACH (bGPDstroke *, gps_target, &gpf->strokes) {
/* Check if the color is editable. */
if ((gps_target == gps) || (ED_gpencil_stroke_color_use(ob, gpl, gps) == false)) {
continue;
}
/* Check if one of the ends is inside target stroke bounding box. */
if (!ED_gpencil_stroke_check_collision(gsc, gps, pt2d_start, radius, diff_mat)) {
continue;
}
if (!ED_gpencil_stroke_check_collision(gsc, gps, pt2d_end, radius, diff_mat)) {
continue;
}
/* Check the distance of the ends with the ends of target stroke to avoid middle contact.
* All is done in 2D plane. */
float pt2d_target_start[2], pt2d_target_end[2];
pt = &gps_target->points[0];
gpencil_point_to_parent_space(pt, diff_mat, &pt_parent);
gpencil_point_to_xy_fl(gsc, gps, &pt_parent, &pt2d_target_start[0], &pt2d_target_start[1]);
pt = &gps_target->points[gps_target->totpoints - 1];
gpencil_point_to_parent_space(pt, diff_mat, &pt_parent);
gpencil_point_to_xy_fl(gsc, gps, &pt_parent, &pt2d_target_end[0], &pt2d_target_end[1]);
if ((len_squared_v2v2(pt2d_start, pt2d_target_start) > radius_sqr) &&
(len_squared_v2v2(pt2d_start, pt2d_target_end) > radius_sqr) &&
(len_squared_v2v2(pt2d_end, pt2d_target_start) > radius_sqr) &&
(len_squared_v2v2(pt2d_end, pt2d_target_end) > radius_sqr)) {
continue;
}
/* Loop all points and check what is the nearest point. */
int i;
for (i = 0, pt = gps_target->points; i < gps_target->totpoints; i++, pt++) {
/* Convert point to 2D. */
float pt2d[2];
gpencil_point_to_parent_space(pt, diff_mat, &pt_parent);
gpencil_point_to_xy_fl(gsc, gps, &pt_parent, &pt2d[0], &pt2d[1]);
/* Check with Start point. */
float dist = len_squared_v2v2(pt2d, pt2d_start);
if ((dist <= radius_sqr) && (dist < dist_min)) {
*r_index = i;
dist_min = dist;
gps_rtn = gps_target;
}
/* Check with End point. */
dist = len_squared_v2v2(pt2d, pt2d_end);
if ((dist <= radius_sqr) && (dist < dist_min)) {
*r_index = i;
dist_min = dist;
gps_rtn = gps_target;
}
}
}
return gps_rtn;
}
/* Join two stroke using a contact point index and trimming the rest. */
bGPDstroke *ED_gpencil_stroke_join_and_trim(
bGPdata *gpd, bGPDframe *gpf, bGPDstroke *gps, bGPDstroke *gps_dst, const int pt_index)
{
if ((gps->totpoints < 1) || (gps_dst->totpoints < 1)) {
return false;
}
BLI_assert(pt_index >= 0 && pt_index < gps_dst->totpoints);
bGPDspoint *pt = NULL;
/* Cannot be cyclic. */
gps->flag &= ~GP_STROKE_CYCLIC;
gps_dst->flag &= ~GP_STROKE_CYCLIC;
/* Trim stroke. */
bGPDstroke *gps_final = gps_dst;
if ((pt_index > 0) && (pt_index < gps_dst->totpoints - 2)) {
/* Untag any pending operation. */
gps_dst->flag &= ~GP_STROKE_TAG;
for (int i = 0; i < gps_dst->totpoints; i++) {
gps_dst->points[i].flag &= ~GP_SPOINT_TAG;
}
/* Delete points of the shorter extreme */
pt = &gps_dst->points[0];
float dist_to_start = BKE_gpencil_stroke_segment_length(gps_dst, 0, pt_index, true);
pt = &gps_dst->points[gps_dst->totpoints - 1];
float dist_to_end = BKE_gpencil_stroke_segment_length(
gps_dst, pt_index, gps_dst->totpoints - 1, true);
if (dist_to_start < dist_to_end) {
for (int i = 0; i < pt_index; i++) {
gps_dst->points[i].flag |= GP_SPOINT_TAG;
}
}
else {
for (int i = pt_index + 1; i < gps_dst->totpoints; i++) {
gps_dst->points[i].flag |= GP_SPOINT_TAG;
}
}
/* Remove tagged points to trim stroke. */
gps_final = BKE_gpencil_stroke_delete_tagged_points(
gpd, gpf, gps_dst, gps_dst->next, GP_SPOINT_TAG, false, 0);
}
/* Join both strokes. */
int totpoint = gps_final->totpoints;
BKE_gpencil_stroke_join(gps_final, gps, false, true);
/* Select the join points and merge if the distance is very small. */
pt = &gps_final->points[totpoint - 1];
pt->flag |= GP_SPOINT_SELECT;
pt = &gps_final->points[totpoint];
pt->flag |= GP_SPOINT_SELECT;
BKE_gpencil_stroke_merge_distance(gpd, gpf, gps_final, 0.01f, false);
/* Unselect all points. */
for (int i = 0; i < gps_final->totpoints; i++) {
gps_final->points[i].flag &= ~GP_SPOINT_SELECT;
}
/* Delete old stroke. */
BLI_remlink(&gpf->strokes, gps);
BKE_gpencil_free_stroke(gps);
return gps_final;
}
/* Close if the distance between extremes is below threshold. */
void ED_gpencil_stroke_close_by_distance(bGPDstroke *gps, const float threshold)
{
if (gps == NULL) {
return;
}
bGPDspoint *pt_start = &gps->points[0];
bGPDspoint *pt_end = &gps->points[gps->totpoints - 1];
const float threshold_sqr = threshold * threshold;
float dist_to_close = len_squared_v3v3(&pt_start->x, &pt_end->x);
if (dist_to_close < threshold_sqr) {
gps->flag |= GP_STROKE_CYCLIC;
BKE_gpencil_stroke_close(gps);
}
}

View File

@ -63,6 +63,8 @@ struct bAnimContext;
struct wmKeyConfig;
struct wmOperator;
#define GPENCIL_MINIMUM_JOIN_DIST 20.0f
/* Reproject stroke modes. */
typedef enum eGP_ReprojectModes {
/* Axis */
@ -366,6 +368,22 @@ bool ED_gpencil_stroke_point_is_inside(struct bGPDstroke *gps,
int mouse[2],
const float diff_mat[4][4]);
struct bGPDstroke *ED_gpencil_stroke_nearest_to_ends(struct bContext *C,
struct GP_SpaceConversion *gsc,
struct bGPDlayer *gpl,
struct bGPDframe *gpf,
struct bGPDstroke *gps,
const float radius,
int *r_index);
struct bGPDstroke *ED_gpencil_stroke_join_and_trim(struct bGPdata *gpd,
struct bGPDframe *gpf,
struct bGPDstroke *gps,
struct bGPDstroke *gps_dst,
const int pt_index);
void ED_gpencil_stroke_close_by_distance(struct bGPDstroke *gps, const float threshold);
#ifdef __cplusplus
}
#endif

View File

@ -2275,6 +2275,8 @@ typedef enum eGPencil_Flags {
GP_TOOL_FLAG_THUMBNAIL_LIST = (1 << 3),
/* Generate wheight data for new strokes */
GP_TOOL_FLAG_CREATE_WEIGHTS = (1 << 4),
/* Automerge with last stroke */
GP_TOOL_FLAG_AUTOMERGE_STROKE = (1 << 5),
} eGPencil_Flags;
/* scene->r.simplify_gpencil */

View File

@ -3273,6 +3273,16 @@ static void rna_def_tool_settings(BlenderRNA *brna)
"if no vertex group selected, weight is not added");
RNA_def_property_update(prop, NC_SCENE | ND_TOOLSETTINGS, NULL);
prop = RNA_def_property(srna, "use_gpencil_automerge_strokes", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, NULL, "gpencil_flags", GP_TOOL_FLAG_AUTOMERGE_STROKE);
RNA_def_property_boolean_default(prop, false);
RNA_def_property_ui_icon(prop, ICON_AUTOMERGE_OFF, 1);
RNA_def_property_ui_text(
prop,
"Automerge",
"Join by distance last drawn stroke with previous strokes in the active layer");
RNA_def_property_clear_flag(prop, PROP_ANIMATABLE);
prop = RNA_def_property(srna, "gpencil_sculpt", PROP_POINTER, PROP_NONE);
RNA_def_property_pointer_sdna(prop, NULL, "gp_sculpt");
RNA_def_property_struct_type(prop, "GPencilSculptSettings");