Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoshua Leung <aligorith@gmail.com>2015-12-13 11:03:13 +0300
committerJoshua Leung <aligorith@gmail.com>2015-12-13 11:03:13 +0300
commita1f87064c4c0cd9e8eafc19c599c4ad4ccdba49f (patch)
treed016e95050f10eda7bc44e1bdfa2f7c4fc0fc024 /source/blender/editors/gpencil
parentd9ee88d126ddb59b68d2561694b522a99e4923b6 (diff)
Grease Pencil: Merge GPencil_Editing_Stage3 branch into master
This commit merges all the work done in the GPencil_Editing_Stage3 branch as of ef2aecf2db981b5344e0d14e7f074f1742b0b2f7 into master. For more details about the changes that this brings, see the WIP release notes: http://wiki.blender.org/index.php/Dev:Ref/Release_Notes/2.77/GPencil
Diffstat (limited to 'source/blender/editors/gpencil')
-rw-r--r--source/blender/editors/gpencil/CMakeLists.txt1
-rw-r--r--source/blender/editors/gpencil/drawgpencil.c25
-rw-r--r--source/blender/editors/gpencil/editaction_gpencil.c239
-rw-r--r--source/blender/editors/gpencil/gpencil_brush.c1691
-rw-r--r--source/blender/editors/gpencil/gpencil_convert.c7
-rw-r--r--source/blender/editors/gpencil/gpencil_data.c218
-rw-r--r--source/blender/editors/gpencil/gpencil_edit.c546
-rw-r--r--source/blender/editors/gpencil/gpencil_intern.h49
-rw-r--r--source/blender/editors/gpencil/gpencil_ops.c93
-rw-r--r--source/blender/editors/gpencil/gpencil_paint.c423
-rw-r--r--source/blender/editors/gpencil/gpencil_select.c11
-rw-r--r--source/blender/editors/gpencil/gpencil_utils.c162
12 files changed, 3050 insertions, 415 deletions
diff --git a/source/blender/editors/gpencil/CMakeLists.txt b/source/blender/editors/gpencil/CMakeLists.txt
index 83a13abb33f..6604d595573 100644
--- a/source/blender/editors/gpencil/CMakeLists.txt
+++ b/source/blender/editors/gpencil/CMakeLists.txt
@@ -40,6 +40,7 @@ set(INC_SYS
set(SRC
drawgpencil.c
editaction_gpencil.c
+ gpencil_brush.c
gpencil_convert.c
gpencil_data.c
gpencil_edit.c
diff --git a/source/blender/editors/gpencil/drawgpencil.c b/source/blender/editors/gpencil/drawgpencil.c
index d17ed22b1ec..f207c71474c 100644
--- a/source/blender/editors/gpencil/drawgpencil.c
+++ b/source/blender/editors/gpencil/drawgpencil.c
@@ -1166,6 +1166,7 @@ static void gp_draw_data_all(Scene *scene, bGPdata *gpd, int offsx, int offsy, i
/* draw grease-pencil sketches to specified 2d-view that uses ibuf corrections */
void ED_gpencil_draw_2dimage(const bContext *C)
{
+ wmWindowManager *wm = CTX_wm_manager(C);
ScrArea *sa = CTX_wm_area(C);
ARegion *ar = CTX_wm_region(C);
Scene *scene = CTX_data_scene(C);
@@ -1218,6 +1219,13 @@ void ED_gpencil_draw_2dimage(const bContext *C)
break;
}
+ if (ED_screen_animation_playing(wm)) {
+ /* don't show onionskins during animation playback/scrub (i.e. it obscures the poses)
+ * OpenGL Renders (i.e. final output), or depth buffer (i.e. not real strokes)
+ */
+ dflag |= GP_DRAWDATA_NO_ONIONS;
+ }
+
/* draw it! */
gp_draw_data_all(scene, gpd, offsx, offsy, sizex, sizey, CFRA, dflag, sa->spacetype);
@@ -1228,6 +1236,7 @@ void ED_gpencil_draw_2dimage(const bContext *C)
* second time with onlyv2d=0 for screen-aligned strokes */
void ED_gpencil_draw_view2d(const bContext *C, bool onlyv2d)
{
+ wmWindowManager *wm = CTX_wm_manager(C);
ScrArea *sa = CTX_wm_area(C);
ARegion *ar = CTX_wm_region(C);
Scene *scene = CTX_data_scene(C);
@@ -1246,6 +1255,8 @@ void ED_gpencil_draw_view2d(const bContext *C, bool onlyv2d)
/* draw it! */
if (onlyv2d) dflag |= (GP_DRAWDATA_ONLYV2D | GP_DRAWDATA_NOSTATUS);
+ if (ED_screen_animation_playing(wm)) dflag |= GP_DRAWDATA_NO_ONIONS;
+
gp_draw_data_all(scene, gpd, 0, 0, ar->winx, ar->winy, CFRA, dflag, sa->spacetype);
/* draw status text (if in screen/pixel-space) */
@@ -1257,7 +1268,7 @@ void ED_gpencil_draw_view2d(const bContext *C, bool onlyv2d)
/* draw grease-pencil sketches to specified 3d-view assuming that matrices are already set correctly
* Note: this gets called twice - first time with only3d=1 to draw 3d-strokes,
* second time with only3d=0 for screen-aligned strokes */
-void ED_gpencil_draw_view3d(Scene *scene, View3D *v3d, ARegion *ar, bool only3d)
+void ED_gpencil_draw_view3d(wmWindowManager *wm, Scene *scene, View3D *v3d, ARegion *ar, bool only3d)
{
bGPdata *gpd;
int dflag = 0;
@@ -1300,13 +1311,15 @@ void ED_gpencil_draw_view3d(Scene *scene, View3D *v3d, ARegion *ar, bool only3d)
dflag |= GP_DRAWDATA_NOSTATUS;
}
+ if ((wm == NULL) || ED_screen_animation_playing(wm)) {
+ /* don't show onionskins during animation playback/scrub (i.e. it obscures the poses)
+ * OpenGL Renders (i.e. final output), or depth buffer (i.e. not real strokes)
+ */
+ dflag |= GP_DRAWDATA_NO_ONIONS;
+ }
+
/* draw it! */
gp_draw_data_all(scene, gpd, offsx, offsy, winx, winy, CFRA, dflag, v3d->spacetype);
-
- /* draw status text (if in screen/pixel-space) */
- if (only3d == false) {
- gp_draw_status_text(gpd, ar);
- }
}
void ED_gpencil_draw_ex(Scene *scene, bGPdata *gpd, int winx, int winy, const int cfra, const char spacetype)
diff --git a/source/blender/editors/gpencil/editaction_gpencil.c b/source/blender/editors/gpencil/editaction_gpencil.c
index a2ba6216f9c..9f96ac6122f 100644
--- a/source/blender/editors/gpencil/editaction_gpencil.c
+++ b/source/blender/editors/gpencil/editaction_gpencil.c
@@ -44,11 +44,15 @@
#include "BKE_fcurve.h"
#include "BKE_gpencil.h"
+#include "BKE_report.h"
+#include "ED_anim_api.h"
#include "ED_gpencil.h"
#include "ED_keyframes_edit.h"
#include "ED_markers.h"
+#include "WM_api.h"
+
/* ***************************************** */
/* NOTE ABOUT THIS FILE:
* This file contains code for editing Grease Pencil data in the Action Editor
@@ -268,7 +272,7 @@ void ED_gplayer_frames_keytype_set(bGPDlayer *gpl, short type)
}
}
-#if 0 // XXX disabled until grease pencil code stabilises again
+
/* -------------------------------------- */
/* Copy and Paste Tools */
/* - The copy/paste buffer currently stores a set of GP_Layers, with temporary
@@ -280,118 +284,155 @@ void ED_gplayer_frames_keytype_set(bGPDlayer *gpl, short type)
*/
/* globals for copy/paste data (like for other copy/paste buffers) */
-ListBase gpcopybuf = {NULL, NULL};
-static int gpcopy_firstframe = 999999999;
+ListBase gp_anim_copybuf = {NULL, NULL};
+static int gp_anim_copy_firstframe = 999999999;
+static int gp_anim_copy_lastframe = -999999999;
+static int gp_anim_copy_cfra = 0;
+
/* This function frees any MEM_calloc'ed copy/paste buffer data */
-void free_gpcopybuf()
+void ED_gpencil_anim_copybuf_free(void)
{
- free_gpencil_layers(&gpcopybuf);
+ free_gpencil_layers(&gp_anim_copybuf);
+ BLI_listbase_clear(&gp_anim_copybuf);
- BLI_listbase_clear(&gpcopybuf);
- gpcopy_firstframe = 999999999;
+ gp_anim_copy_firstframe = 999999999;
+ gp_anim_copy_lastframe = -999999999;
+ gp_anim_copy_cfra = 0;
}
+
/* This function adds data to the copy/paste buffer, freeing existing data first
* Only the selected GP-layers get their selected keyframes copied.
+ *
+ * Returns whether the copy operation was successful or not
*/
-void copy_gpdata()
+bool ED_gpencil_anim_copybuf_copy(bAnimContext *ac)
{
- ListBase act_data = {NULL, NULL};
- bActListElem *ale;
+ ListBase anim_data = {NULL, NULL};
+ bAnimListElem *ale;
int filter;
- void *data;
- short datatype;
- /* clear buffer first */
- free_gpcopybuf();
+ Scene *scene = ac->scene;
- /* get data */
- data = get_action_context(&datatype);
- if (data == NULL) return;
- if (datatype != ACTCONT_GPENCIL) return;
- /* filter data */
- filter = (ACTFILTER_VISIBLE | ACTFILTER_SEL);
- actdata_filter(&act_data, filter, data, datatype);
+ /* clear buffer first */
+ ED_gpencil_anim_copybuf_free();
- /* assume that each of these is an ipo-block */
- for (ale = act_data.first; ale; ale = ale->next) {
- bGPDlayer *gpls, *gpln;
- bGPDframe *gpf, *gpfn;
-
- /* get new layer to put into buffer */
- gpls = (bGPDlayer *)ale->data;
- gpln = MEM_callocN(sizeof(bGPDlayer), "GPCopyPasteLayer");
-
- BLI_listbase_clear(&gpln->frames);
- BLI_strncpy(gpln->info, gpls->info, sizeof(gpln->info));
-
- BLI_addtail(&gpcopybuf, gpln);
+ /* filter data */
+ filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_NODUPLIS);
+ ANIM_animdata_filter(ac, &anim_data, filter, ac->data, ac->datatype);
+
+ /* assume that each of these is a GP layer */
+ for (ale = anim_data.first; ale; ale = ale->next) {
+ ListBase copied_frames = {NULL, NULL};
+ bGPDlayer *gpl = (bGPDlayer *)ale->data;
+ bGPDframe *gpf;
/* loop over frames, and copy only selected frames */
- for (gpf = gpls->frames.first; gpf; gpf = gpf->next) {
+ for (gpf = gpl->frames.first; gpf; gpf = gpf->next) {
/* if frame is selected, make duplicate it and its strokes */
if (gpf->flag & GP_FRAME_SELECT) {
- /* add frame to buffer */
- gpfn = gpencil_frame_duplicate(gpf);
- BLI_addtail(&gpln->frames, gpfn);
+ /* make a copy of this frame */
+ bGPDframe *new_frame = gpencil_frame_duplicate(gpf);
+ BLI_addtail(&copied_frames, new_frame);
- /* check if this is the earliest frame encountered so far */
- if (gpf->framenum < gpcopy_firstframe)
- gpcopy_firstframe = gpf->framenum;
+ /* extend extents for keyframes encountered */
+ if (gpf->framenum < gp_anim_copy_firstframe)
+ gp_anim_copy_firstframe = gpf->framenum;
+ if (gpf->framenum > gp_anim_copy_lastframe)
+ gp_anim_copy_lastframe = gpf->framenum;
}
}
+
+ /* create a new layer in buffer if there were keyframes here */
+ if (BLI_listbase_is_empty(&copied_frames) == false) {
+ bGPDlayer *new_layer = MEM_callocN(sizeof(bGPDlayer), "GPCopyPasteLayer");
+ BLI_addtail(&gp_anim_copybuf, new_layer);
+
+ /* move over copied frames */
+ BLI_movelisttolist(&new_layer->frames, &copied_frames);
+ BLI_assert(copied_frames.first == NULL);
+
+ /* make a copy of the layer's name - for name-based matching later... */
+ BLI_strncpy(new_layer->info, gpl->info, sizeof(new_layer->info));
+ }
}
+ /* in case 'relative' paste method is used */
+ gp_anim_copy_cfra = CFRA;
+
+ /* clean up */
+ ANIM_animdata_freelist(&anim_data);
+
/* check if anything ended up in the buffer */
- if (ELEM(NULL, gpcopybuf.first, gpcopybuf.last))
- error("Nothing copied to buffer");
+ if (ELEM(NULL, gp_anim_copybuf.first, gp_anim_copybuf.last)) {
+ BKE_report(ac->reports, RPT_ERROR, "No keyframes copied to keyframes copy/paste buffer");
+ return false;
+ }
- /* free temp memory */
- BLI_freelistN(&act_data);
+ /* report success */
+ return true;
}
-void paste_gpdata(Scene *scene)
+
+/* Pastes keyframes from buffer, and reports success */
+bool ED_gpencil_anim_copybuf_paste(bAnimContext *ac, const short offset_mode)
{
- ListBase act_data = {NULL, NULL};
- bActListElem *ale;
+ ListBase anim_data = {NULL, NULL};
+ bAnimListElem *ale;
int filter;
- void *data;
- short datatype;
- const int offset = (CFRA - gpcopy_firstframe);
- short no_name = 0;
+ Scene *scene = ac->scene;
+ bool no_name = false;
+ int offset = 0;
/* check if buffer is empty */
- if (ELEM(NULL, gpcopybuf.first, gpcopybuf.last)) {
- error("No data in buffer to paste");
- return;
+ if (BLI_listbase_is_empty(&gp_anim_copybuf)) {
+ BKE_report(ac->reports, RPT_ERROR, "No data in buffer to paste");
+ return false;
}
+
/* check if single channel in buffer (disregard names if so) */
- if (gpcopybuf.first == gpcopybuf.last)
- no_name = 1;
+ if (gp_anim_copybuf.first == gp_anim_copybuf.last) {
+ no_name = true;
+ }
- /* get data */
- data = get_action_context(&datatype);
- if (data == NULL) return;
- if (datatype != ACTCONT_GPENCIL) return;
+ /* methods of offset (eKeyPasteOffset) */
+ switch (offset_mode) {
+ case KEYFRAME_PASTE_OFFSET_CFRA_START:
+ offset = (CFRA - gp_anim_copy_firstframe);
+ break;
+ case KEYFRAME_PASTE_OFFSET_CFRA_END:
+ offset = (CFRA - gp_anim_copy_lastframe);
+ break;
+ case KEYFRAME_PASTE_OFFSET_CFRA_RELATIVE:
+ offset = (CFRA - gp_anim_copy_cfra);
+ break;
+ case KEYFRAME_PASTE_OFFSET_NONE:
+ offset = 0;
+ break;
+ }
+
/* filter data */
- filter = (ACTFILTER_VISIBLE | ACTFILTER_SEL | ACTFILTER_FOREDIT);
- actdata_filter(&act_data, filter, data, datatype);
+ // TODO: try doing it with selection, then without selection imits
+ filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE | ANIMFILTER_SEL | ANIMFILTER_FOREDIT | ANIMFILTER_NODUPLIS);
+ ANIM_animdata_filter(ac, &anim_data, filter, ac->data, ac->datatype);
/* from selected channels */
- for (ale = act_data.first; ale; ale = ale->next) {
+ for (ale = anim_data.first; ale; ale = ale->next) {
bGPDlayer *gpld = (bGPDlayer *)ale->data;
bGPDlayer *gpls = NULL;
bGPDframe *gpfs, *gpf;
+
/* find suitable layer from buffer to use to paste from */
- for (gpls = gpcopybuf.first; gpls; gpls = gpls->next) {
+ for (gpls = gp_anim_copybuf.first; gpls; gpls = gpls->next) {
/* check if layer name matches */
- if ((no_name) || STREQ(gpls->info, gpld->info))
+ if ((no_name) || STREQ(gpls->info, gpld->info)) {
break;
+ }
}
/* this situation might occur! */
@@ -407,58 +448,21 @@ void paste_gpdata(Scene *scene)
gpf = gpencil_layer_getframe(gpld, gpfs->framenum, 1);
if (gpf) {
bGPDstroke *gps, *gpsn;
- ScrArea *sa;
-
- /* get area that gp-data comes from */
- //sa = gpencil_data_findowner((bGPdata *)ale->owner);
- sa = NULL;
- /* this should be the right frame... as it may be a pre-existing frame,
+ /* This should be the right frame... as it may be a pre-existing frame,
* must make sure that only compatible stroke types get copied over
- * - we cannot just add a duplicate frame, as that would cause errors
- * - need to check for compatible types to minimize memory usage (copying 'junk' over)
+ * - We cannot just add a duplicate frame, as that would cause errors
+ * - For now, we don't check if the types will be compatible since we
+ * don't have enough info to do so. Instead, we simply just paste,
+ * af it works, it will show up.
*/
for (gps = gpfs->strokes.first; gps; gps = gps->next) {
- short stroke_ok;
+ /* make a copy of stroke, then of its points array */
+ gpsn = MEM_dupallocN(gps);
+ gpsn->points = MEM_dupallocN(gps->points);
- /* if there's an area, check that it supports this type of stroke */
- if (sa) {
- stroke_ok = 0;
-
- /* check if spacetype supports this type of stroke
- * - NOTE: must sync this with gp_paint_initstroke() in gpencil.c
- */
- switch (sa->spacetype) {
- case SPACE_VIEW3D: /* 3D-View: either screen-aligned or 3d-space */
- if ((gps->flag == 0) || (gps->flag & GP_STROKE_3DSPACE))
- stroke_ok = 1;
- break;
-
- case SPACE_NODE: /* Nodes Editor: either screen-aligned or view-aligned */
- case SPACE_IMAGE: /* Image Editor: either screen-aligned or view\image-aligned */
- case SPACE_CLIP: /* Image Editor: either screen-aligned or view\image-aligned */
- if ((gps->flag == 0) || (gps->flag & GP_STROKE_2DSPACE))
- stroke_ok = 1;
- break;
-
- case SPACE_SEQ: /* Sequence Editor: either screen-aligned or view-aligned */
- if ((gps->flag == 0) || (gps->flag & GP_STROKE_2DIMAGE))
- stroke_ok = 1;
- break;
- }
- }
- else
- stroke_ok = 1;
-
- /* if stroke is ok, we make a copy of this stroke and add to frame */
- if (stroke_ok) {
- /* make a copy of stroke, then of its points array */
- gpsn = MEM_dupallocN(gps);
- gpsn->points = MEM_dupallocN(gps->points);
-
- /* append stroke to frame */
- BLI_addtail(&gpf->strokes, gpsn);
- }
+ /* append stroke to frame */
+ BLI_addtail(&gpf->strokes, gpsn);
}
/* if no strokes (i.e. new frame) added, free gpf */
@@ -471,13 +475,10 @@ void paste_gpdata(Scene *scene)
}
}
- /* free temp memory */
- BLI_freelistN(&act_data);
-
- /* undo and redraw stuff */
- BIF_undo_push("Paste Grease Pencil Frames");
+ /* clean up */
+ ANIM_animdata_freelist(&anim_data);
+ return true;
}
-#endif /* XXX disabled until Grease Pencil code stabilises again... */
/* -------------------------------------- */
/* Snap Tools */
diff --git a/source/blender/editors/gpencil/gpencil_brush.c b/source/blender/editors/gpencil/gpencil_brush.c
new file mode 100644
index 00000000000..ecc78dfc6b4
--- /dev/null
+++ b/source/blender/editors/gpencil/gpencil_brush.c
@@ -0,0 +1,1691 @@
+/*
+ * ***** 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) 2015, Blender Foundation
+ * This is a new part of Blender
+ *
+ * Contributor(s): Joshua Leung
+ *
+ * ***** END GPL LICENSE BLOCK *****
+ *
+ * Brush based operators for editing Grease Pencil strokes
+ */
+
+/** \file blender/editors/gpencil/gpencil_edit.c
+ * \ingroup edgpencil
+ */
+
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stddef.h>
+#include <math.h>
+
+#include "MEM_guardedalloc.h"
+
+#include "BLI_blenlib.h"
+#include "BLI_ghash.h"
+#include "BLI_math.h"
+#include "BLI_rand.h"
+#include "BLI_utildefines.h"
+
+#include "BLT_translation.h"
+
+#include "DNA_scene_types.h"
+#include "DNA_screen_types.h"
+#include "DNA_space_types.h"
+#include "DNA_view3d_types.h"
+#include "DNA_gpencil_types.h"
+
+#include "BKE_context.h"
+#include "BKE_global.h"
+#include "BKE_gpencil.h"
+#include "BKE_library.h"
+#include "BKE_report.h"
+#include "BKE_screen.h"
+
+#include "UI_interface.h"
+
+#include "WM_api.h"
+#include "WM_types.h"
+
+#include "RNA_access.h"
+#include "RNA_define.h"
+#include "RNA_enum_types.h"
+
+#include "UI_view2d.h"
+
+#include "ED_gpencil.h"
+#include "ED_screen.h"
+#include "ED_view3d.h"
+
+#include "BIF_gl.h"
+#include "BIF_glutil.h"
+
+#include "gpencil_intern.h"
+
+/* ************************************************ */
+/* General Brush Editing Context */
+
+/* Context for brush operators */
+typedef struct tGP_BrushEditData {
+ /* Current editor/region/etc. */
+ /* NOTE: This stuff is mainly needed to handle 3D view projection stuff... */
+ Scene *scene;
+
+ ScrArea *sa;
+ ARegion *ar;
+
+ /* Current GPencil datablock */
+ bGPdata *gpd;
+
+ /* Brush Settings */
+ GP_BrushEdit_Settings *settings;
+ GP_EditBrush_Data *brush;
+
+ eGP_EditBrush_Types brush_type;
+ eGP_EditBrush_Flag flag;
+
+ /* Space Conversion Data */
+ GP_SpaceConversion gsc;
+
+
+ /* Is the brush currently painting? */
+ bool is_painting;
+
+ /* Start of new sculpt stroke */
+ bool first;
+
+ /* Current frame */
+ int cfra;
+
+
+ /* Brush Runtime Data: */
+ /* - position and pressure
+ * - the *_prev variants are the previous values
+ */
+ int mval[2], mval_prev[2];
+ float pressure, pressure_prev;
+
+ /* - effect vector (e.g. 2D/3D translation for grab brush) */
+ float dvec[3];
+
+ /* brush geometry (bounding box) */
+ rcti brush_rect;
+
+ /* Custom data for certain brushes */
+ /* - map from bGPDstroke's to structs containing custom data about those strokes */
+ GHash *stroke_customdata;
+ /* - general customdata */
+ void *customdata;
+
+
+ /* Timer for in-place accumulation of brush effect */
+ wmTimer *timer;
+ bool timerTick; /* is this event from a timer */
+} tGP_BrushEditData;
+
+
+/* Callback for performing some brush operation on a single point */
+typedef bool (*GP_BrushApplyCb)(tGP_BrushEditData *gso, bGPDstroke *gps, int i,
+ const int radius, const int co[2]);
+
+/* ************************************************ */
+/* Utility Functions */
+
+/* Context ---------------------------------------- */
+
+/* Get the sculpting settings */
+static GP_BrushEdit_Settings *gpsculpt_get_settings(Scene *scene)
+{
+ return &scene->toolsettings->gp_sculpt;
+}
+
+/* Get the active brush */
+static GP_EditBrush_Data *gpsculpt_get_brush(Scene *scene)
+{
+ GP_BrushEdit_Settings *gset = &scene->toolsettings->gp_sculpt;
+ return &gset->brush[gset->brushtype];
+}
+
+/* Brush Operations ------------------------------- */
+
+/* Invert behaviour of brush? */
+static bool gp_brush_invert_check(tGP_BrushEditData *gso)
+{
+ /* The basic setting is the brush's setting (from the panel) */
+ bool invert = ((gso->brush->flag & GP_EDITBRUSH_FLAG_INVERT) != 0);
+
+ /* During runtime, the user can hold down the Ctrl key to invert the basic behaviour */
+ if (gso->flag & GP_EDITBRUSH_FLAG_INVERT) {
+ invert ^= true;
+ }
+
+ return invert;
+}
+
+/* Compute strength of effect */
+static float gp_brush_influence_calc(tGP_BrushEditData *gso, const int radius, const int co[2])
+{
+ GP_EditBrush_Data *brush = gso->brush;
+
+ /* basic strength factor from brush settings */
+ float influence = brush->strength;
+
+ /* use pressure? */
+ if (brush->flag & GP_EDITBRUSH_FLAG_USE_PRESSURE) {
+ influence *= gso->pressure;
+ }
+
+ /* distance fading */
+ if (brush->flag & GP_EDITBRUSH_FLAG_USE_FALLOFF) {
+ float distance = (float)len_v2v2_int(gso->mval, co);
+ float fac;
+
+ CLAMP(distance, 0.0f, (float)radius);
+ fac = 1.0f - (distance / (float)radius);
+
+ influence *= fac;
+ }
+
+ /* return influence */
+ return influence;
+}
+
+/* ************************************************ */
+/* 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.
+ */
+
+/* ----------------------------------------------- */
+/* Smooth Brush */
+
+/* A simple (but slower + inaccurate) smooth-brush implementation to test the algorithm for stroke smoothing */
+static bool gp_brush_smooth_apply(tGP_BrushEditData *gso, bGPDstroke *gps, int i,
+ const int radius, const int co[2])
+{
+ GP_EditBrush_Data *brush = gso->brush;
+ bGPDspoint *pt = &gps->points[i];
+ float inf = gp_brush_influence_calc(gso, radius, co);
+ float pressure = 0.0f;
+ float sco[3] = {0.0f};
+
+ /* Do nothing if not enough points to smooth out */
+ if (gps->totpoints <= 2) {
+ return false;
+ }
+
+ /* Only affect endpoints by a fraction of the normal strength,
+ * to prevent the stroke from shrinking too much
+ */
+ if ((i == 0) || (i == gps->totpoints - 1)) {
+ inf *= 0.1f;
+ }
+
+ /* Compute smoothed coordinate by taking the ones nearby */
+ /* XXX: This is potentially slow, and suffers from accumulation error as earlier points are handled before later ones */
+ {
+ // XXX: this is hardcoded to look at 2 points on either side of the current one (i.e. 5 items total)
+ const int steps = 2;
+ const float average_fac = 1.0f / (float)(steps * 2 + 1);
+ int step;
+
+ /* add the point itself */
+ madd_v3_v3fl(sco, &pt->x, average_fac);
+
+ if (brush->flag & GP_EDITBRUSH_FLAG_SMOOTH_PRESSURE) {
+ pressure += pt->pressure * average_fac;
+ }
+
+ /* n-steps before/after current point */
+ // XXX: review how the endpoints are treated by this algorithm
+ // XXX: falloff measures should also introduce some weighting variations, so that further-out points get less weight
+ for (step = 1; step <= steps; step++) {
+ bGPDspoint *pt1, *pt2;
+ int before = i - step;
+ int after = i + step;
+
+ CLAMP_MIN(before, 0);
+ CLAMP_MAX(after, gps->totpoints - 1);
+
+ pt1 = &gps->points[before];
+ pt2 = &gps->points[after];
+
+ /* add both these points to the average-sum (s += p[i]/n) */
+ madd_v3_v3fl(sco, &pt1->x, average_fac);
+ madd_v3_v3fl(sco, &pt2->x, average_fac);
+
+ /* do pressure too? */
+ if (brush->flag & GP_EDITBRUSH_FLAG_SMOOTH_PRESSURE) {
+ pressure += pt1->pressure * average_fac;
+ pressure += pt2->pressure * average_fac;
+ }
+ }
+ }
+
+ /* Based on influence factor, blend between original and optimal smoothed coordinate */
+ interp_v3_v3v3(&pt->x, &pt->x, sco, inf);
+
+ if (brush->flag & GP_EDITBRUSH_FLAG_SMOOTH_PRESSURE) {
+ pt->pressure = pressure;
+ }
+
+ return true;
+}
+
+/* ----------------------------------------------- */
+/* Line Thickness Brush */
+
+/* Make lines thicker or thinner by the specified amounts */
+static bool gp_brush_thickness_apply(tGP_BrushEditData *gso, bGPDstroke *gps, int i,
+ const int radius, const int co[2])
+{
+ bGPDspoint *pt = gps->points + i;
+ float inf;
+
+ /* Compute strength of effect
+ * - We divide the strength by 10, so that users can set "sane" values.
+ * Otherwise, good default values are in the range of 0.093
+ */
+ inf = gp_brush_influence_calc(gso, radius, co) / 10.0f;
+
+ /* apply */
+ // XXX: this is much too strong, and it should probably do some smoothing with the surrounding stuff
+ if (gp_brush_invert_check(gso)) {
+ /* make line thinner - reduce stroke pressure */
+ pt->pressure -= inf;
+ }
+ else {
+ /* make line thicker - increase stroke pressure */
+ pt->pressure += inf;
+ }
+
+ /* Pressure should stay within [0.0, 1.0]
+ * However, it is nice for volumetric strokes to be able to exceed
+ * the upper end of this range. Therefore, we don't actually clamp
+ * down on the upper end.
+ */
+ if (pt->pressure < 0.0f)
+ pt->pressure = 0.0f;
+
+ return true;
+}
+
+
+/* ----------------------------------------------- */
+/* Grab Brush */
+
+/* Custom data per stroke for the Grab Brush
+ *
+ * This basically defines the strength of the effect for each
+ * affected stroke point that was within the initial range of
+ * the brush region.
+ */
+typedef struct tGPSB_Grab_StrokeData {
+ /* array of indices to corresponding points in the stroke */
+ int *points;
+ /* array of influence weights for each of the included points */
+ float *weights;
+
+ /* capacity of the arrays */
+ int capacity;
+ /* actual number of items currently stored */
+ int size;
+} tGPSB_Grab_StrokeData;
+
+/* initialise custom data for handling this stroke */
+static void gp_brush_grab_stroke_init(tGP_BrushEditData *gso, bGPDstroke *gps)
+{
+ tGPSB_Grab_StrokeData *data = NULL;
+
+ BLI_assert(gps->totpoints > 0);
+
+ /* Check if there are buffers already (from a prior run) */
+ if (BLI_ghash_haskey(gso->stroke_customdata, gps)) {
+ /* Ensure that the caches are empty
+ * - Since we reuse these between different strokes, we don't
+ * want the previous invocation's data polluting the arrays
+ */
+ data = BLI_ghash_lookup(gso->stroke_customdata, gps);
+ BLI_assert(data != NULL);
+
+ data->size = 0; /* minimum requirement - so that we can repopulate again */
+
+ memset(data->points, 0, sizeof(int) * data->capacity);
+ memset(data->weights, 0, sizeof(float) * data->capacity);
+ }
+ else {
+ /* Create new instance */
+ data = MEM_callocN(sizeof(tGPSB_Grab_StrokeData), "GP Stroke Grab Data");
+
+ data->capacity = gps->totpoints;
+ data->size = 0;
+
+ data->points = MEM_callocN(sizeof(int) * data->capacity, "GP Stroke Grab Indices");
+ data->weights = MEM_callocN(sizeof(float) * data->capacity, "GP Stroke Grab Weights");
+
+ /* hook up to the cache */
+ BLI_ghash_insert(gso->stroke_customdata, gps, data);
+ }
+}
+
+/* store references to stroke points in the initial stage */
+static bool gp_brush_grab_store_points(tGP_BrushEditData *gso, bGPDstroke *gps, int i,
+ const int radius, const int co[2])
+{
+ tGPSB_Grab_StrokeData *data = BLI_ghash_lookup(gso->stroke_customdata, gps);
+ float inf = gp_brush_influence_calc(gso, radius, co);
+
+ BLI_assert(data != NULL);
+ BLI_assert(data->size < data->capacity);
+
+ /* insert this point into the set of affected points */
+ data->points[data->size] = i;
+ data->weights[data->size] = inf;
+ data->size++;
+
+ /* done */
+ return true;
+}
+
+/* Compute effect vector for grab brush */
+static void gp_brush_grab_calc_dvec(tGP_BrushEditData *gso)
+{
+ /* Convert mouse-movements to movement vector */
+ // TODO: incorporate pressure into this?
+ // XXX: screen-space strokes in 3D space will suffer!
+ if (gso->sa->spacetype == SPACE_VIEW3D) {
+ View3D *v3d = gso->sa->spacedata.first;
+ RegionView3D *rv3d = gso->ar->regiondata;
+ float *rvec = ED_view3d_cursor3d_get(gso->scene, v3d);
+ float zfac = ED_view3d_calc_zfac(rv3d, rvec, NULL);
+
+ float mval_f[2];
+
+ /* convert from 2D screenspace to 3D... */
+ mval_f[0] = (float)(gso->mval[0] - gso->mval_prev[0]);
+ mval_f[1] = (float)(gso->mval[1] - gso->mval_prev[1]);
+
+ ED_view3d_win_to_delta(gso->ar, mval_f, gso->dvec, zfac);
+ }
+ else {
+ /* 2D - just copy */
+ // XXX: view2d?
+ gso->dvec[0] = (float)(gso->mval[0] - gso->mval_prev[0]);
+ gso->dvec[1] = (float)(gso->mval[1] - gso->mval_prev[1]);
+ gso->dvec[2] = 0.0f; /* unused */
+ }
+}
+
+/* Apply grab transform to all relevant points of the affected strokes */
+static void gp_brush_grab_apply_cached(tGP_BrushEditData *gso, bGPDstroke *gps)
+{
+ tGPSB_Grab_StrokeData *data = BLI_ghash_lookup(gso->stroke_customdata, gps);
+ int i;
+
+ /* Apply dvec to all of the stored points */
+ for (i = 0; i < data->size; i++) {
+ bGPDspoint *pt = &gps->points[data->points[i]];
+ float delta[3] = {0.0f};
+
+ /* adjust the amount of displacement to apply */
+ mul_v3_v3fl(delta, gso->dvec, data->weights[i]);
+
+ /* apply */
+ add_v3_v3(&pt->x, delta);
+ }
+}
+
+/* free customdata used for handling this stroke */
+static void gp_brush_grab_stroke_free(void *ptr)
+{
+ tGPSB_Grab_StrokeData *data = (tGPSB_Grab_StrokeData *)ptr;
+
+ /* free arrays */
+ MEM_freeN(data->points);
+ MEM_freeN(data->weights);
+
+ /* ... and this item itself, since it was also allocated */
+ MEM_freeN(data);
+}
+
+/* ----------------------------------------------- */
+/* Push Brush */
+/* NOTE: Depends on gp_brush_grab_calc_dvec() */
+
+static bool gp_brush_push_apply(tGP_BrushEditData *gso, bGPDstroke *gps, int i,
+ const int radius, const int co[2])
+{
+ bGPDspoint *pt = gps->points + i;
+ float inf = gp_brush_influence_calc(gso, radius, co);
+ float delta[3] = {0.0f};
+
+ /* adjust the amount of displacement to apply */
+ mul_v3_v3fl(delta, gso->dvec, inf);
+
+ /* apply */
+ add_v3_v3(&pt->x, delta);
+
+ /* done */
+ return true;
+}
+
+/* ----------------------------------------------- */
+/* Pinch Brush */
+
+/* Compute reference midpoint for the brush - this is what we'll be moving towards */
+static void gp_brush_calc_midpoint(tGP_BrushEditData *gso)
+{
+ if (gso->sa->spacetype == SPACE_VIEW3D) {
+ /* Convert mouse position to 3D space
+ * See: gpencil_paint.c :: gp_stroke_convertcoords()
+ */
+ View3D *v3d = gso->sa->spacedata.first;
+ RegionView3D *rv3d = gso->ar->regiondata;
+ float *rvec = ED_view3d_cursor3d_get(gso->scene, v3d);
+ float zfac = ED_view3d_calc_zfac(rv3d, rvec, NULL);
+
+ float mval_f[2] = {UNPACK2(gso->mval)};
+ float mval_prj[2];
+ float dvec[3];
+
+
+ if (ED_view3d_project_float_global(gso->ar, rvec, mval_prj, V3D_PROJ_TEST_NOP) == V3D_PROJ_RET_OK) {
+ sub_v2_v2v2(mval_f, mval_prj, mval_f);
+ ED_view3d_win_to_delta(gso->ar, mval_f, dvec, zfac);
+ sub_v3_v3v3(gso->dvec, rvec, dvec);
+ }
+ else {
+ zero_v3(gso->dvec);
+ }
+ }
+ else {
+ /* Just 2D coordinates */
+ // XXX: fix View2D offsets later
+ gso->dvec[0] = (float)gso->mval[0];
+ gso->dvec[1] = (float)gso->mval[1];
+ gso->dvec[2] = 0.0f;
+ }
+}
+
+/* Shrink distance between midpoint and this point... */
+static bool gp_brush_pinch_apply(tGP_BrushEditData *gso, bGPDstroke *gps, int i,
+ const int radius, const int co[2])
+{
+ bGPDspoint *pt = gps->points + i;
+ float fac, inf;
+ float vec[3];
+
+ /* Scale down standard influence value to get it more manageable...
+ * - No damping = Unmanageable at > 0.5 strength
+ * - Div 10 = Not enough effect
+ * - Div 5 = Happy medium... (by trial and error)
+ */
+ inf = gp_brush_influence_calc(gso, radius, co) / 5.0f;
+
+ /* 1) Make this point relative to the cursor/midpoint (dvec) */
+ sub_v3_v3v3(vec, &pt->x, gso->dvec);
+
+ /* 2) Shrink the distance by pulling the point towards the midpoint
+ * (0.0 = at midpoint, 1 = at edge of brush region)
+ * OR
+ * Increase the distance (if inverting the brush action!)
+ */
+ if (gp_brush_invert_check(gso)) {
+ /* Inflate (inverse) */
+ fac = 1.0f + (inf * inf); /* squared to temper the effect... */
+ }
+ else {
+ /* Shrink (default) */
+ fac = 1.0f - (inf * inf); /* squared to temper the effect... */
+ }
+ mul_v3_fl(vec, fac);
+
+ /* 3) Translate back to original space, with the shrinkage applied */
+ add_v3_v3v3(&pt->x, gso->dvec, vec);
+
+ /* done */
+ return true;
+}
+
+/* ----------------------------------------------- */
+/* Twist Brush - Rotate Around midpoint */
+
+/* Take the screenspace coordinates of the point, rotate this around the brush midpoint,
+ * convert the rotated point and convert it into "data" space
+ */
+
+static bool gp_brush_twist_apply(tGP_BrushEditData *gso, bGPDstroke *gps, int i,
+ const int radius, const int co[2])
+{
+ bGPDspoint *pt = gps->points + i;
+ float tco[2], rco[2], nco[2];
+ float rmat[2][2];
+ float angle, inf;
+
+ /* Angle to rotate by */
+ inf = gp_brush_influence_calc(gso, radius, co);
+ angle = DEG2RADF(1.0f) * inf;
+
+ if (gp_brush_invert_check(gso)) {
+ /* invert angle that we rotate by */
+ angle *= -1;
+ }
+
+ /* Express position of point relative to cursor, ready to rotate */
+ tco[0] = (float)(co[0] - gso->mval[0]);
+ tco[1] = (float)(co[1] - gso->mval[1]);
+
+ /* Rotate point in 2D */
+ angle_to_mat2(rmat, angle);
+ mul_v2_m2v2(rco, rmat, tco);
+
+ /* Convert back to screen-coordinates */
+ nco[0] = rco[0] + (float)gso->mval[0];
+ nco[1] = rco[1] + (float)gso->mval[1];
+
+#if 0
+ printf("C: %d %d | P: %d %d -> t: %f %f -> r: %f %f x %f -> %f %f\n",
+ gso->mval[0], gso->mval[1], co[0], co[1],
+ tco[0], tco[1],
+ rco[0], rco[1], angle,
+ nco[0], nco[1]);
+#endif
+
+ /* convert to dataspace */
+ if (gps->flag & GP_STROKE_3DSPACE) {
+ /* 3D: Project to 3D space */
+ if (gso->sa->spacetype == SPACE_VIEW3D) {
+ // XXX: this conversion process sometimes introduces noise to the data -> some parts don't seem to move at all, while others get random offsets
+ gp_point_xy_to_3d(&gso->gsc, gso->scene, nco, &pt->x);
+ }
+ else {
+ /* ERROR */
+ BLI_assert("3D stroke being sculpted in non-3D view");
+ }
+ }
+ else {
+ /* 2D: As-is */
+ // XXX: v2d scaling/offset?
+ copy_v2_v2(&pt->x, nco);
+ }
+
+ /* done */
+ return true;
+}
+
+
+/* ----------------------------------------------- */
+/* Randomise Brush */
+
+/* Apply some random jitter to the point */
+static bool gp_brush_randomise_apply(tGP_BrushEditData *gso, bGPDstroke *gps, int i,
+ const int radius, const int co[2])
+{
+ bGPDspoint *pt = gps->points + i;
+
+ /* Amount of jitter to apply depends on the distance of the point to the cursor,
+ * as well as the strength of the brush
+ */
+ const float inf = gp_brush_influence_calc(gso, radius, co) / 2.0f;
+ const float fac = BLI_frand() * inf;
+
+ /* Jitter is applied perpendicular to the mouse movement vector
+ * - We compute all effects in screenspace (since it's easier)
+ * and then project these to get the points/distances in
+ * viewspace as needed
+ */
+ float mvec[2], svec[2], nco[2];
+
+ /* mouse movement in ints -> floats */
+ mvec[0] = (float)(gso->mval[0] - gso->mval_prev[0]);
+ mvec[1] = (float)(gso->mval[1] - gso->mval_prev[1]);
+
+ /* rotate mvec by 90 degrees... */
+ svec[0] = -mvec[1];
+ svec[1] = mvec[0];
+
+ //printf("svec = %f %f, ", svec[0], svec[1]);
+
+ /* scale the displacement by the random displacement, and apply */
+ if (BLI_frand() > 0.5f) {
+ mul_v2_fl(svec, -fac);
+ }
+ else {
+ mul_v2_fl(svec, fac);
+ }
+
+ nco[0] = (float)co[0] + svec[0];
+ nco[1] = (float)co[1] + svec[1];
+
+ //printf("%f %f (%f), nco = {%f %f}, co = %d %d\n", svec[0], svec[1], fac, nco[0], nco[1], co[0], co[1]);
+
+ /* convert to dataspace */
+ if (gps->flag & GP_STROKE_3DSPACE) {
+ /* 3D: Project to 3D space */
+ if (gso->sa->spacetype == SPACE_VIEW3D) {
+ gp_point_xy_to_3d(&gso->gsc, gso->scene, nco, &pt->x);
+ }
+ else {
+ /* ERROR */
+ BLI_assert("3D stroke being sculpted in non-3D view");
+ }
+ }
+ else {
+ /* 2D: As-is */
+ // XXX: v2d scaling/offset?
+ copy_v2_v2(&pt->x, nco);
+ }
+
+ /* done */
+ return true;
+}
+
+/* ************************************************ */
+/* Non Callback-Based Brushes */
+
+/* Clone Brush ------------------------------------- */
+/* How this brush currently works:
+ * - If this is start of the brush stroke, paste immediately under the cursor
+ * by placing the midpoint of the buffer strokes under the cursor now
+ *
+ * - Otherwise, in:
+ * "Stamp Mode" - Move the newly pasted strokes so that their center
+ * "Continuous" - Repeatedly just paste new copies for where the brush is now
+ */
+
+/* Custom state data for clone brush */
+typedef struct tGPSB_CloneBrushData {
+ /* midpoint of the strokes on the clipboard */
+ float buffer_midpoint[3];
+
+ /* number of strokes in the paste buffer (and/or to be created each time) */
+ size_t totitems;
+
+ /* for "stamp" mode, the currently pasted brushes */
+ bGPDstroke **new_strokes;
+} tGPSB_CloneBrushData;
+
+/* Initialise "clone" brush data */
+static void gp_brush_clone_init(bContext *C, tGP_BrushEditData *gso)
+{
+ tGPSB_CloneBrushData *data;
+ bGPDstroke *gps;
+
+ /* init custom data */
+ gso->customdata = data = MEM_callocN(sizeof(tGPSB_CloneBrushData), "CloneBrushData");
+
+ /* compute midpoint of strokes on clipboard */
+ for (gps = gp_strokes_copypastebuf.first; gps; gps = gps->next) {
+ if (ED_gpencil_stroke_can_use(C, gps)) {
+ const float dfac = 1.0f / ((float)gps->totpoints);
+ float mid[3] = {0.0f};
+
+ bGPDspoint *pt;
+ int i;
+
+ /* compute midpoint of this stroke */
+ for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
+ float co[3];
+
+ mul_v3_v3fl(co, &pt->x, dfac);
+ add_v3_v3(mid, co);
+ }
+
+ /* combine this stroke's data with the main data */
+ add_v3_v3(data->buffer_midpoint, mid);
+ data->totitems++;
+ }
+ }
+
+ /* Divide the midpoint by the number of strokes, to finish averaging it */
+ if (data->totitems > 1) {
+ mul_v3_fl(data->buffer_midpoint, 1.0f / (float)data->totitems);
+ }
+
+ /* Create a buffer for storing the current strokes */
+ if (1 /*gso->brush->mode == GP_EDITBRUSH_CLONE_MODE_STAMP*/) {
+ data->new_strokes = MEM_callocN(sizeof(bGPDstroke *) * data->totitems, "cloned strokes ptr array");
+ }
+}
+
+/* Free custom data used for "clone" brush */
+static void gp_brush_clone_free(tGP_BrushEditData *gso)
+{
+ tGPSB_CloneBrushData *data = gso->customdata;
+
+ /* free strokes array */
+ if (data->new_strokes) {
+ MEM_freeN(data->new_strokes);
+ data->new_strokes = NULL;
+ }
+
+ /* free the customdata itself */
+ MEM_freeN(data);
+ gso->customdata = NULL;
+}
+
+/* Create new copies of the strokes on the clipboard */
+static void gp_brush_clone_add(bContext *C, tGP_BrushEditData *gso)
+{
+ tGPSB_CloneBrushData *data = gso->customdata;
+
+ Scene *scene = gso->scene;
+ bGPDlayer *gpl = CTX_data_active_gpencil_layer(C);
+ bGPDframe *gpf = gpencil_layer_getframe(gpl, CFRA, true);
+ bGPDstroke *gps;
+
+ float delta[3];
+ size_t strokes_added = 0;
+
+ /* Compute amount to offset the points by */
+ /* NOTE: This assumes that screenspace strokes are NOT used in the 3D view... */
+
+ gp_brush_calc_midpoint(gso); /* this puts the cursor location into gso->dvec */
+ sub_v3_v3v3(delta, gso->dvec, data->buffer_midpoint);
+
+ /* Copy each stroke into the layer */
+ for (gps = gp_strokes_copypastebuf.first; gps; gps = gps->next) {
+ if (ED_gpencil_stroke_can_use(C, gps)) {
+ bGPDstroke *new_stroke;
+ bGPDspoint *pt;
+ int i;
+
+ /* Make a new stroke */
+ new_stroke = MEM_dupallocN(gps);
+
+ new_stroke->points = MEM_dupallocN(gps->points);
+ new_stroke->next = new_stroke->prev = NULL;
+
+ BLI_addtail(&gpf->strokes, new_stroke);
+
+ /* Adjust all the stroke's points, so that the strokes
+ * get pasted relative to where the cursor is now
+ */
+ for (i = 0, pt = new_stroke->points; i < new_stroke->totpoints; i++, pt++) {
+ /* assume that the delta can just be applied, and then everything works */
+ add_v3_v3(&pt->x, delta);
+ }
+
+ /* Store ref for later */
+ if ((data->new_strokes) && (strokes_added < data->totitems)) {
+ data->new_strokes[strokes_added] = new_stroke;
+ strokes_added++;
+ }
+ }
+ }
+}
+
+/* Move newly-added strokes around - "Stamp" mode of the Clone brush */
+static void gp_brush_clone_adjust(bContext *C, tGP_BrushEditData *gso)
+{
+ tGPSB_CloneBrushData *data = gso->customdata;
+ size_t snum;
+
+ /* Compute the amount of movement to apply (overwrites dvec) */
+ gp_brush_grab_calc_dvec(gso);
+
+ /* For each of the stored strokes, apply the offset to each point */
+ /* NOTE: Again this assumes that in the 3D view, we only have 3d space and not screenspace strokes... */
+ for (snum = 0; snum < data->totitems; snum++) {
+ bGPDstroke *gps = data->new_strokes[snum];
+ bGPDspoint *pt;
+ int i;
+
+ for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
+ if (gso->brush->flag & GP_EDITBRUSH_FLAG_USE_FALLOFF) {
+ /* "Smudge" Effect when falloff is enabled */
+ float delta[3] = {0.0f};
+ int sco[2] = {0};
+ float influence;
+
+ /* compute influence on point */
+ gp_point_to_xy(&gso->gsc, gps, pt, &sco[0], &sco[1]);
+ influence = gp_brush_influence_calc(gso, gso->brush->size, sco);
+
+ /* adjust the amount of displacement to apply */
+ mul_v3_v3fl(delta, gso->dvec, influence);
+
+ /* apply */
+ add_v3_v3(&pt->x, delta);
+ }
+ else {
+ /* Just apply the offset - All points move perfectly in sync with the cursor */
+ add_v3_v3(&pt->x, gso->dvec);
+ }
+ }
+ }
+}
+
+/* Entrypoint for applying "clone" brush */
+static bool gpsculpt_brush_apply_clone(bContext *C, tGP_BrushEditData *gso)
+{
+ /* Which "mode" are we operating in? */
+ if (gso->first) {
+ /* Create initial clones */
+ gp_brush_clone_add(C, gso);
+ }
+ else {
+ /* Stamp or Continous Mode */
+ if (1 /*gso->brush->mode == GP_EDITBRUSH_CLONE_MODE_STAMP*/) {
+ /* Stamp - Proceed to translate the newly added strokes */
+ gp_brush_clone_adjust(C, gso);
+ }
+ else {
+ /* Continuous - Just keep pasting everytime we move */
+ /* TODO: The spacing of repeat should be controlled using a "stepsize" or similar property? */
+ gp_brush_clone_add(C, gso);
+ }
+ }
+
+ return true;
+}
+
+/* ************************************************ */
+/* Cursor drawing */
+
+/* Helper callback for drawing the cursor itself */
+static void gp_brush_drawcursor(bContext *C, int x, int y, void *UNUSED(customdata))
+{
+ GP_EditBrush_Data *brush = gpsculpt_get_brush(CTX_data_scene(C));
+
+ if (brush) {
+ glPushMatrix();
+
+ glTranslatef((float)x, (float)y, 0.0f);
+
+ /* TODO: toggle between add and remove? */
+ glColor4ub(255, 255, 255, 128);
+
+ glEnable(GL_LINE_SMOOTH);
+ glEnable(GL_BLEND);
+
+ glutil_draw_lined_arc(0.0, M_PI * 2.0, brush->size, 40);
+
+ glDisable(GL_BLEND);
+ glDisable(GL_LINE_SMOOTH);
+
+ glPopMatrix();
+ }
+}
+
+/* Turn brush cursor in on/off */
+static void gpencil_toggle_brush_cursor(bContext *C, bool enable)
+{
+ GP_BrushEdit_Settings *gset = gpsculpt_get_settings(CTX_data_scene(C));
+
+ if (gset->paintcursor && !enable) {
+ /* clear cursor */
+ WM_paint_cursor_end(CTX_wm_manager(C), gset->paintcursor);
+ gset->paintcursor = NULL;
+ }
+ else if (enable) {
+ /* enable cursor */
+ gset->paintcursor = WM_paint_cursor_activate(CTX_wm_manager(C),
+ NULL,
+ gp_brush_drawcursor, NULL);
+ }
+}
+
+
+/* ************************************************ */
+/* Header Info for GPencil Sculpt */
+
+static void gpsculpt_brush_header_set(bContext *C, tGP_BrushEditData *gso)
+{
+ const char *brush_name = NULL;
+ char str[256] = "";
+
+ RNA_enum_name(rna_enum_gpencil_sculpt_brush_items, gso->brush_type, &brush_name);
+
+ BLI_snprintf(str, sizeof(str),
+ IFACE_("GPencil Sculpt: %s Stroke | LMB to paint | RMB/Escape to Exit"
+ " | Ctrl to Invert Action | Wheel Up/Down for Size "
+ " | Shift-Wheel Up/Down for Strength"),
+ (brush_name) ? brush_name : "<?>");
+
+ ED_area_headerprint(CTX_wm_area(C), str);
+}
+
+/* ************************************************ */
+/* Grease Pencil Sculpting Operator */
+
+/* Init/Exit ----------------------------------------------- */
+
+static bool gpsculpt_brush_init(bContext *C, wmOperator *op)
+{
+ Scene *scene = CTX_data_scene(C);
+ tGP_BrushEditData *gso;
+
+ /* setup operator data */
+ gso = MEM_callocN(sizeof(tGP_BrushEditData), "tGP_BrushEditData");
+ op->customdata = gso;
+
+ /* store state */
+ gso->settings = gpsculpt_get_settings(scene);
+ gso->brush = gpsculpt_get_brush(scene);
+
+ gso->brush_type = gso->settings->brushtype;
+
+
+ gso->is_painting = false;
+ gso->first = true;
+
+ gso->gpd = ED_gpencil_data_get_active(C);
+ gso->cfra = INT_MAX; /* NOTE: So that first stroke will get handled in init_stroke() */
+
+ gso->scene = scene;
+
+ gso->sa = CTX_wm_area(C);
+ gso->ar = CTX_wm_region(C);
+
+ /* initialise custom data for brushes */
+ switch (gso->brush_type) {
+ case GP_EDITBRUSH_TYPE_CLONE:
+ {
+ bGPDstroke *gps;
+ bool found = false;
+
+ /* check that there are some usable strokes in the buffer */
+ for (gps = gp_strokes_copypastebuf.first; gps; gps = gps->next) {
+ if (ED_gpencil_stroke_can_use(C, gps)) {
+ found = true;
+ break;
+ }
+ }
+
+ if (found == false) {
+ /* STOP HERE! Nothing to paste! */
+ BKE_report(op->reports, RPT_ERROR,
+ "Copy some strokes to the clipboard before using the Clone brush to paste copies of them");
+
+ MEM_freeN(gso);
+ op->customdata = NULL;
+ return false;
+ }
+ else {
+ /* initialise customdata */
+ gp_brush_clone_init(C, gso);
+ }
+ break;
+ }
+
+ case GP_EDITBRUSH_TYPE_GRAB:
+ {
+ /* initialise the cache needed for this brush */
+ gso->stroke_customdata = BLI_ghash_ptr_new("GP Grab Brush - Strokes Hash");
+ break;
+ }
+
+ /* Others - No customdata needed */
+ default:
+ break;
+ }
+
+
+ /* setup space conversions */
+ gp_point_conversion_init(C, &gso->gsc);
+
+ /* update header */
+ gpsculpt_brush_header_set(C, gso);
+
+ /* setup cursor drawing */
+ WM_cursor_modal_set(CTX_wm_window(C), BC_CROSSCURSOR);
+ gpencil_toggle_brush_cursor(C, true);
+
+ return true;
+}
+
+static void gpsculpt_brush_exit(bContext *C, wmOperator *op)
+{
+ tGP_BrushEditData *gso = op->customdata;
+ wmWindow *win = CTX_wm_window(C);
+
+ /* free brush-specific data */
+ switch (gso->brush_type) {
+ case GP_EDITBRUSH_TYPE_GRAB:
+ {
+ /* Free per-stroke customdata
+ * - Keys don't need to be freed, as those are the strokes
+ * - Values assigned to those keys do, as they are custom structs
+ */
+ BLI_ghash_free(gso->stroke_customdata, NULL, gp_brush_grab_stroke_free);
+ break;
+ }
+
+ case GP_EDITBRUSH_TYPE_CLONE:
+ {
+ /* Free customdata */
+ gp_brush_clone_free(gso);
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ /* unregister timer (only used for realtime) */
+ if (gso->timer) {
+ WM_event_remove_timer(CTX_wm_manager(C), win, gso->timer);
+ }
+
+ /* disable cursor and headerprints */
+ ED_area_headerprint(CTX_wm_area(C), NULL);
+ WM_cursor_modal_restore(win);
+ gpencil_toggle_brush_cursor(C, false);
+
+ /* free operator data */
+ MEM_freeN(gso);
+ op->customdata = NULL;
+}
+
+/* poll callback for stroke sculpting operator(s) */
+static int gpsculpt_brush_poll(bContext *C)
+{
+ /* NOTE: this is a bit slower, but is the most accurate... */
+ return CTX_DATA_COUNT(C, editable_gpencil_strokes) != 0;
+}
+
+/* Init Sculpt Stroke ---------------------------------- */
+
+static void gpsculpt_brush_init_stroke(tGP_BrushEditData *gso)
+{
+ Scene *scene = gso->scene;
+ bGPdata *gpd = gso->gpd;
+ bGPDlayer *gpl;
+ int cfra = CFRA;
+
+ /* only try to add a new frame if this is the first stroke, or the frame has changed */
+ if ((gpd == NULL) || (cfra == gso->cfra))
+ return;
+
+ /* go through each layer, and ensure that we've got a valid frame to use */
+ for (gpl = gpd->layers.first; gpl; gpl = gpl->next) {
+ /* only editable and visible layers are considered */
+ if ((gpl->flag & (GP_LAYER_HIDE | GP_LAYER_LOCKED)) == 0 &&
+ (gpl->actframe != NULL))
+ {
+ bGPDframe *gpf = gpl->actframe;
+
+ /* Make a new frame to work on if the layer's frame and the current scene frame don't match up
+ * - This is useful when animating as it saves that "uh-oh" moment when you realize you've
+ * spent too much time editing the wrong frame...
+ */
+ // XXX: should this be allowed when framelock is enabled?
+ if (gpf->framenum != cfra) {
+ gpencil_frame_addcopy(gpl, cfra);
+ }
+ }
+ }
+
+ /* save off new current frame, so that next update works fine */
+ gso->cfra = cfra;
+}
+
+/* Apply ----------------------------------------------- */
+
+/* Apply brush operation to points in this stroke */
+static bool gpsculpt_brush_do_stroke(tGP_BrushEditData *gso, bGPDstroke *gps, GP_BrushApplyCb apply)
+{
+ GP_SpaceConversion *gsc = &gso->gsc;
+ rcti *rect = &gso->brush_rect;
+ const int radius = gso->brush->size;
+
+ bGPDspoint *pt1, *pt2;
+ int pc1[2] = {0};
+ int pc2[2] = {0};
+ int i;
+ bool include_last = false;
+ bool changed = false;
+
+ if (gps->totpoints == 1) {
+ gp_point_to_xy(gsc, gps, gps->points, &pc1[0], &pc1[1]);
+
+ /* 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 */
+ if (len_v2v2_int(gso->mval, pc1) <= radius) {
+ /* apply operation to this point */
+ changed = apply(gso, gps, 0, radius, pc1);
+ }
+ }
+ }
+ else {
+ /* Loop over the points in the stroke, checking for intersections
+ * - an intersection means that we touched the stroke
+ */
+ 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 (gso->settings->flag & GP_BRUSHEDIT_FLAG_SELECT_MASK) {
+ if (!(pt1->flag & GP_SPOINT_SELECT) && !(pt2->flag & GP_SPOINT_SELECT)) {
+ include_last = false;
+ continue;
+ }
+ }
+
+ gp_point_to_xy(gsc, gps, pt1, &pc1[0], &pc1[1]);
+ gp_point_to_xy(gsc, gps, pt2, &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
+ * eraser region (either within stroke painted, or on its lines)
+ * - this assumes that linewidth is irrelevant
+ */
+ if (gp_stroke_inside_circle(gso->mval, gso->mval_prev, radius, pc1[0], pc1[1], pc2[0], pc2[1])) {
+ /* Apply operation to these points */
+ bool ok = false;
+
+ /* To each point individually... */
+ ok = apply(gso, gps, i, radius, 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) {
+ ok |= apply(gso, gps, i + 1, radius, pc2);
+ include_last = false;
+ }
+ else {
+ include_last = true;
+ }
+
+ changed |= ok;
+ }
+ 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)
+ */
+ changed |= apply(gso, gps, i, radius, pc1);
+ include_last = false;
+ }
+ }
+ }
+ }
+
+ return changed;
+}
+
+/* Perform two-pass brushes which modify the existing strokes */
+static bool gpsculpt_brush_apply_standard(bContext *C, tGP_BrushEditData *gso)
+{
+ bool changed = false;
+
+ /* Calculate brush-specific data which applies equally to all points */
+ switch (gso->brush_type) {
+ case GP_EDITBRUSH_TYPE_GRAB: /* Grab points */
+ case GP_EDITBRUSH_TYPE_PUSH: /* Push points */
+ {
+ /* calculate amount of displacement to apply */
+ gp_brush_grab_calc_dvec(gso);
+ break;
+ }
+
+ case GP_EDITBRUSH_TYPE_PINCH: /* Pinch points */
+ //case GP_EDITBRUSH_TYPE_TWIST: /* Twist points around midpoint */
+ {
+ /* calculate midpoint of the brush (in data space) */
+ gp_brush_calc_midpoint(gso);
+ break;
+ }
+
+ case GP_EDITBRUSH_TYPE_RANDOMISE: /* Random jitter */
+ {
+ /* compute the displacement vector for the cursor (in data space) */
+ gp_brush_grab_calc_dvec(gso);
+ break;
+ }
+
+ default:
+ break;
+ }
+
+
+ /* Find visible strokes, and perform operations on those if hit */
+ CTX_DATA_BEGIN(C, bGPDstroke *, gps, editable_gpencil_strokes)
+ {
+ switch (gso->brush_type) {
+ case GP_EDITBRUSH_TYPE_SMOOTH: /* Smooth strokes */
+ {
+ changed |= gpsculpt_brush_do_stroke(gso, gps, gp_brush_smooth_apply);
+ break;
+ }
+
+ case GP_EDITBRUSH_TYPE_THICKNESS: /* Adjust stroke thickness */
+ {
+ changed |= gpsculpt_brush_do_stroke(gso, gps, gp_brush_thickness_apply);
+ break;
+ }
+
+ case GP_EDITBRUSH_TYPE_GRAB: /* Grab points */
+ {
+ if (gso->first) {
+ /* First time this brush stroke is being applied:
+ * 1) Prepare data buffers (init/clear) for this stroke
+ * 2) Use the points now under the cursor
+ */
+ gp_brush_grab_stroke_init(gso, gps);
+ changed |= gpsculpt_brush_do_stroke(gso, gps, gp_brush_grab_store_points);
+ }
+ else {
+ /* Apply effect to the stored points */
+ gp_brush_grab_apply_cached(gso, gps);
+ changed |= true;
+ }
+ break;
+ }
+
+ case GP_EDITBRUSH_TYPE_PUSH: /* Push points */
+ {
+ changed |= gpsculpt_brush_do_stroke(gso, gps, gp_brush_push_apply);
+ break;
+ }
+
+ case GP_EDITBRUSH_TYPE_PINCH: /* Pinch points */
+ {
+ changed |= gpsculpt_brush_do_stroke(gso, gps, gp_brush_pinch_apply);
+ break;
+ }
+
+ case GP_EDITBRUSH_TYPE_TWIST: /* Twist points around midpoint */
+ {
+ changed |= gpsculpt_brush_do_stroke(gso, gps, gp_brush_twist_apply);
+ break;
+ }
+
+ case GP_EDITBRUSH_TYPE_RANDOMISE: /* Apply jitter */
+ {
+ changed |= gpsculpt_brush_do_stroke(gso, gps, gp_brush_randomise_apply);
+ break;
+ }
+
+ default:
+ printf("ERROR: Unknown type of GPencil Sculpt brush - %d\n", gso->brush_type);
+ break;
+ }
+ }
+ CTX_DATA_END;
+
+ return changed;
+}
+
+/* Calculate settings for applying brush */
+static void gpsculpt_brush_apply(bContext *C, wmOperator *op, PointerRNA *itemptr)
+{
+ tGP_BrushEditData *gso = op->customdata;
+ const int radius = 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_EDITBRUSH_FLAG_INVERT;
+ else
+ gso->flag &= ~GP_EDITBRUSH_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;
+
+
+ /* Apply brush */
+ if (gso->brush_type == GP_EDITBRUSH_TYPE_CLONE) {
+ changed = gpsculpt_brush_apply_clone(C, gso);
+ }
+ else {
+ changed = gpsculpt_brush_apply_standard(C, gso);
+ }
+
+
+ /* Updates */
+ if (changed) {
+ 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 gpsculpt_brush_apply_event(bContext *C, wmOperator *op, const wmEvent *event)
+{
+ tGP_BrushEditData *gso = op->customdata;
+ PointerRNA itemptr;
+ float mouse[2];
+ int tablet = 0;
+
+ 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) */
+ if (event->tablet_data) {
+ const wmTabletData *wmtab = event->tablet_data;
+ float pressure = wmtab->Pressure;
+
+ tablet = (wmtab->Active != EVT_TABLET_NONE);
+
+ /* special exception here for too high pressure values on first touch in
+ * windows for some tablets: clamp the values to be sane
+ */
+ if (tablet && (pressure >= 0.99f)) {
+ pressure = 1.0f;
+ }
+ RNA_float_set(&itemptr, "pressure", pressure);
+ }
+ else {
+ RNA_float_set(&itemptr, "pressure", 1.0f);
+ }
+
+ /* apply */
+ gpsculpt_brush_apply(C, op, &itemptr);
+}
+
+/* reapply */
+static int gpsculpt_brush_exec(bContext *C, wmOperator *op)
+{
+ if (!gpsculpt_brush_init(C, op))
+ return OPERATOR_CANCELLED;
+
+ RNA_BEGIN(op->ptr, itemptr, "stroke")
+ {
+ gpsculpt_brush_apply(C, op, &itemptr);
+ }
+ RNA_END;
+
+ gpsculpt_brush_exit(C, op);
+
+ return OPERATOR_FINISHED;
+}
+
+
+/* start modal painting */
+static int gpsculpt_brush_invoke(bContext *C, wmOperator *op, const wmEvent *event)
+{
+ tGP_BrushEditData *gso = NULL;
+ const bool is_modal = RNA_boolean_get(op->ptr, "wait_for_input");
+ bool needs_timer = false;
+ float brush_rate = 0.0f;
+
+ /* init painting data */
+ if (!gpsculpt_brush_init(C, op))
+ return OPERATOR_CANCELLED;
+
+ gso = op->customdata;
+
+ /* initialise type-specific data (used for the entire session) */
+ switch (gso->brush_type) {
+ /* Brushes requiring timer... */
+ case GP_EDITBRUSH_TYPE_THICKNESS:
+ brush_rate = 0.01f; // XXX: hardcoded
+ needs_timer = true;
+ break;
+
+ case GP_EDITBRUSH_TYPE_PINCH:
+ brush_rate = 0.001f; // XXX: hardcoded
+ needs_timer = true;
+ break;
+
+ case GP_EDITBRUSH_TYPE_TWIST:
+ brush_rate = 0.01f; // XXX: hardcoded
+ needs_timer = true;
+ break;
+
+ default:
+ break;
+ }
+
+ /* register timer for increasing influence by hovering over an area */
+ if (needs_timer) {
+ gso->timer = WM_event_add_timer(CTX_wm_manager(C), CTX_wm_window(C), TIMER, brush_rate);
+ }
+
+ /* register modal handler */
+ WM_event_add_modal_handler(C, op);
+
+ /* start drawing immediately? */
+ if (is_modal == false) {
+ ARegion *ar = CTX_wm_region(C);
+
+ /* ensure that we'll have a new frame to draw on */
+ gpsculpt_brush_init_stroke(gso);
+
+ /* apply first dab... */
+ gso->is_painting = true;
+ gpsculpt_brush_apply_event(C, op, event);
+
+ /* redraw view with feedback */
+ ED_region_tag_redraw(ar);
+ }
+
+ return OPERATOR_RUNNING_MODAL;
+}
+
+/* painting - handle events */
+static int gpsculpt_brush_modal(bContext *C, wmOperator *op, const wmEvent *event)
+{
+ tGP_BrushEditData *gso = op->customdata;
+ const bool is_modal = RNA_boolean_get(op->ptr, "wait_for_input");
+ bool redraw_region = 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 */
+ gpsculpt_brush_apply_event(C, op, event);
+
+ /* force redraw, so that the cursor will at least be valid */
+ redraw_region = true;
+ break;
+
+ /* Timer Tick - Only if this was our own timer */
+ case TIMER:
+ if (event->customdata == gso->timer) {
+ gso->timerTick = true;
+ gpsculpt_brush_apply_event(C, op, event);
+ gso->timerTick = false;
+ }
+ break;
+
+ /* Painting mbut release = Stop painting (back to idle) */
+ case LEFTMOUSE:
+ //BLI_assert(event->val == KM_RELEASE);
+ if (is_modal) {
+ /* go back to idling... */
+ gso->is_painting = false;
+ }
+ else {
+ /* end sculpt session, since we're not modal */
+ gso->is_painting = false;
+
+ gpsculpt_brush_exit(C, op);
+ return OPERATOR_FINISHED;
+ }
+ break;
+
+ /* Abort painting if any of the usual things are tried */
+ case MIDDLEMOUSE:
+ case RIGHTMOUSE:
+ case ESCKEY:
+ gpsculpt_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;
+
+ gpsculpt_brush_init_stroke(gso);
+ gpsculpt_brush_apply_event(C, op, event);
+ break;
+
+ /* Exit modal operator, based on the "standard" ops */
+ case RIGHTMOUSE:
+ case ESCKEY:
+ gpsculpt_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;
+
+ /* Adjust brush settings */
+ /* FIXME: Step increments and modifier keys are hardcoded here! */
+ case WHEELUPMOUSE:
+ case PADPLUSKEY:
+ if (event->shift) {
+ /* increase strength */
+ gso->brush->strength += 0.05f;
+ CLAMP_MAX(gso->brush->strength, 1.0f);
+ }
+ else {
+ /* increase brush size */
+ gso->brush->size += 3;
+ CLAMP_MAX(gso->brush->size, 300);
+ }
+
+ redraw_region = true;
+ break;
+
+ case WHEELDOWNMOUSE:
+ case PADMINUS:
+ if (event->shift) {
+ /* decrease strength */
+ gso->brush->strength -= 0.05f;
+ CLAMP_MIN(gso->brush->strength, 0.0f);
+ }
+ else {
+ /* decrease brush size */
+ gso->brush->size -= 3;
+ CLAMP_MIN(gso->brush->size, 1);
+ }
+
+ redraw_region = true;
+ break;
+
+ /* Change Frame - Allowed */
+ case LEFTARROWKEY:
+ case RIGHTARROWKEY:
+ case UPARROWKEY:
+ case DOWNARROWKEY:
+ return OPERATOR_PASS_THROUGH;
+
+ /* Unhandled event */
+ default:
+ break;
+ }
+ }
+
+ /* Redraw region? */
+ if (redraw_region) {
+ ARegion *ar = CTX_wm_region(C);
+ ED_region_tag_redraw(ar);
+ }
+
+ return OPERATOR_RUNNING_MODAL;
+}
+
+
+/* Operator --------------------------------------------- */
+
+void GPENCIL_OT_brush_paint(wmOperatorType *ot)
+{
+ PropertyRNA *prop;
+
+ /* identifiers */
+ ot->name = "Stroke Sculpt";
+ ot->idname = "GPENCIL_OT_brush_paint";
+ ot->description = "Apply tweaks to strokes by painting over the strokes"; // XXX
+
+ /* api callbacks */
+ ot->exec = gpsculpt_brush_exec;
+ ot->invoke = gpsculpt_brush_invoke;
+ ot->modal = gpsculpt_brush_modal;
+ ot->cancel = gpsculpt_brush_exit;
+ ot->poll = gpsculpt_brush_poll;
+
+ /* flags */
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO | OPTYPE_BLOCKING;
+
+ /* properties */
+ RNA_def_collection_runtime(ot->srna, "stroke", &RNA_OperatorStrokeElement, "Stroke", "");
+
+ prop = RNA_def_boolean(ot->srna, "wait_for_input", true, "Wait for Input",
+ "Enter a mini 'sculpt-mode' if enabled, otherwise, exit after drawing a single stroke");
+ RNA_def_property_flag(prop, PROP_SKIP_SAVE);
+}
+
+/* ************************************************ */
diff --git a/source/blender/editors/gpencil/gpencil_convert.c b/source/blender/editors/gpencil/gpencil_convert.c
index e8d73eaffdf..ab02000a200 100644
--- a/source/blender/editors/gpencil/gpencil_convert.c
+++ b/source/blender/editors/gpencil/gpencil_convert.c
@@ -77,6 +77,7 @@
#include "RNA_access.h"
#include "RNA_define.h"
+#include "UI_resources.h"
#include "UI_view2d.h"
#include "ED_gpencil.h"
@@ -106,9 +107,9 @@ enum {
/* RNA enum define */
static EnumPropertyItem prop_gpencil_convertmodes[] = {
- {GP_STROKECONVERT_PATH, "PATH", 0, "Path", ""},
- {GP_STROKECONVERT_CURVE, "CURVE", 0, "Bezier Curve", ""},
- {GP_STROKECONVERT_POLY, "POLY", 0, "Polygon Curve", ""},
+ {GP_STROKECONVERT_PATH, "PATH", ICON_CURVE_PATH, "Path", "Animation path"},
+ {GP_STROKECONVERT_CURVE, "CURVE", ICON_CURVE_BEZCURVE, "Bezier Curve", "Smooth Bezier curve"},
+ {GP_STROKECONVERT_POLY, "POLY", ICON_MESH_DATA, "Polygon Curve", "Bezier curve with straight-line segments (vector handles)"},
{0, NULL, 0, NULL, NULL}
};
diff --git a/source/blender/editors/gpencil/gpencil_data.c b/source/blender/editors/gpencil/gpencil_data.c
index de966776645..4f03a53e736 100644
--- a/source/blender/editors/gpencil/gpencil_data.c
+++ b/source/blender/editors/gpencil/gpencil_data.c
@@ -59,12 +59,14 @@
#include "BKE_screen.h"
#include "UI_interface.h"
+#include "UI_resources.h"
#include "WM_api.h"
#include "WM_types.h"
#include "RNA_access.h"
#include "RNA_define.h"
+#include "RNA_enum_types.h"
#include "ED_gpencil.h"
@@ -444,4 +446,220 @@ void GPENCIL_OT_reveal(wmOperatorType *ot)
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
+/* ***************** Lock/Unlock All Layers ************************ */
+
+static int gp_lock_all_exec(bContext *C, wmOperator *UNUSED(op))
+{
+ bGPdata *gpd = ED_gpencil_data_get_active(C);
+ bGPDlayer *gpl;
+
+ /* sanity checks */
+ if (gpd == NULL)
+ return OPERATOR_CANCELLED;
+
+ /* make all layers non-editable */
+ for (gpl = gpd->layers.first; gpl; gpl = gpl->next) {
+ gpl->flag |= GP_LAYER_LOCKED;
+ }
+
+ /* notifiers */
+ WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
+
+ return OPERATOR_FINISHED;
+}
+
+void GPENCIL_OT_lock_all(wmOperatorType *ot)
+{
+ /* identifiers */
+ ot->name = "Lock All Layers";
+ ot->idname = "GPENCIL_OT_lock_all";
+ ot->description = "Lock all Grease Pencil layers to prevent them from being accidentally modified";
+
+ /* callbacks */
+ ot->exec = gp_lock_all_exec;
+ ot->poll = gp_reveal_poll; /* XXX: could use dedicated poll later */
+
+ /* flags */
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+}
+
+/* -------------------------- */
+
+static int gp_unlock_all_exec(bContext *C, wmOperator *UNUSED(op))
+{
+ bGPdata *gpd = ED_gpencil_data_get_active(C);
+ bGPDlayer *gpl;
+
+ /* sanity checks */
+ if (gpd == NULL)
+ return OPERATOR_CANCELLED;
+
+ /* make all layers editable again */
+ for (gpl = gpd->layers.first; gpl; gpl = gpl->next) {
+ gpl->flag &= ~GP_LAYER_LOCKED;
+ }
+
+ /* notifiers */
+ WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
+
+ return OPERATOR_FINISHED;
+}
+
+void GPENCIL_OT_unlock_all(wmOperatorType *ot)
+{
+ /* identifiers */
+ ot->name = "Unlock All Layers";
+ ot->idname = "GPENCIL_OT_unlock_all";
+ ot->description = "unlock all Grease Pencil layers so that they can be edited";
+
+ /* callbacks */
+ ot->exec = gp_unlock_all_exec;
+ ot->poll = gp_reveal_poll; /* XXX: could use dedicated poll later */
+
+ /* flags */
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+}
+
+/* ********************** Isolate Layer **************************** */
+
+static int gp_isolate_layer_exec(bContext *C, wmOperator *op)
+{
+ bGPdata *gpd = ED_gpencil_data_get_active(C);
+ bGPDlayer *layer = gpencil_layer_getactive(gpd);
+ bGPDlayer *gpl;
+ int flags = GP_LAYER_LOCKED;
+ bool isolate = false;
+
+ if (RNA_boolean_get(op->ptr, "affect_visibility"))
+ flags |= GP_LAYER_HIDE;
+
+ if (ELEM(NULL, gpd, layer)) {
+ BKE_report(op->reports, RPT_ERROR, "No active layer to isolate");
+ return OPERATOR_CANCELLED;
+ }
+
+ /* Test whether to isolate or clear all flags */
+ for (gpl = gpd->layers.first; gpl; gpl = gpl->next) {
+ /* Skip if this is the active layer */
+ if (gpl == layer)
+ continue;
+
+ /* If the flags aren't set, that means that the layer is
+ * not alone, so we have some layers to isolate still
+ */
+ if ((gpl->flag & flags) == 0) {
+ isolate = true;
+ break;
+ }
+ }
+
+ /* Set/Clear flags as appropriate */
+ /* TODO: Include onionskinning on this list? */
+ if (isolate) {
+ /* Set flags on all "other" layers */
+ for (gpl = gpd->layers.first; gpl; gpl = gpl->next) {
+ if (gpl == layer)
+ continue;
+ else
+ gpl->flag |= flags;
+ }
+ }
+ else {
+ /* Clear flags - Restore everything else */
+ for (gpl = gpd->layers.first; gpl; gpl = gpl->next) {
+ gpl->flag &= ~flags;
+ }
+ }
+
+ /* notifiers */
+ WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
+
+ return OPERATOR_FINISHED;
+}
+
+void GPENCIL_OT_layer_isolate(wmOperatorType *ot)
+{
+ /* identifiers */
+ ot->name = "Isolate Layer";
+ ot->idname = "GPENCIL_OT_layer_isolate";
+ ot->description = "Toggle whether the active layer is the only one that can be edited and/or visible";
+
+ /* callbacks */
+ ot->exec = gp_isolate_layer_exec;
+ ot->poll = gp_active_layer_poll;
+
+ /* flags */
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+
+ /* properties */
+ RNA_def_boolean(ot->srna, "affect_visibility", false, "Affect Visibility",
+ "In addition to toggling the editability, also affect the visibility");
+}
+
+/* ********************** Change Layer ***************************** */
+
+static int gp_layer_change_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(evt))
+{
+ uiPopupMenu *pup;
+ uiLayout *layout;
+
+ /* call the menu, which will call this operator again, hence the canceled */
+ pup = UI_popup_menu_begin(C, op->type->name, ICON_NONE);
+ layout = UI_popup_menu_layout(pup);
+ uiItemsEnumO(layout, "GPENCIL_OT_layer_change", "layer");
+ UI_popup_menu_end(C, pup);
+
+ return OPERATOR_INTERFACE;
+}
+
+static int gp_layer_change_exec(bContext *C, wmOperator *op)
+{
+ bGPdata *gpd = CTX_data_gpencil_data(C);
+ bGPDlayer *gpl = NULL;
+ int layer_num = RNA_enum_get(op->ptr, "layer");
+
+ /* Get layer or create new one */
+ if (layer_num == -1) {
+ /* Create layer */
+ gpl = gpencil_layer_addnew(gpd, DATA_("GP_Layer"), true);
+ }
+ else {
+ /* Try to get layer */
+ gpl = BLI_findlink(&gpd->layers, layer_num);
+
+ if (gpl == NULL) {
+ BKE_reportf(op->reports, RPT_ERROR, "Cannot change to non-existent layer (index = %d)", layer_num);
+ return OPERATOR_CANCELLED;
+ }
+ }
+
+ /* Set active layer */
+ gpencil_layer_setactive(gpd, gpl);
+
+ /* updates */
+ WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
+
+ return OPERATOR_FINISHED;
+}
+
+void GPENCIL_OT_layer_change(wmOperatorType *ot)
+{
+ /* identifiers */
+ ot->name = "Change Layer";
+ ot->idname = "GPENCIL_OT_layer_change";
+ ot->description = "Change active Grease Pencil layer";
+
+ /* callbacks */
+ ot->invoke = gp_layer_change_invoke;
+ ot->exec = gp_layer_change_exec;
+ ot->poll = gp_active_layer_poll;
+
+ /* flags */
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+
+ /* gp layer to use (dynamic enum) */
+ ot->prop = RNA_def_enum(ot->srna, "layer", DummyRNA_DEFAULT_items, 0, "Grease Pencil Layer", "");
+ RNA_def_enum_funcs(ot->prop, ED_gpencil_layers_with_new_enum_itemf);
+}
+
/* ************************************************ */
diff --git a/source/blender/editors/gpencil/gpencil_edit.c b/source/blender/editors/gpencil/gpencil_edit.c
index 5c37a0a5b60..9c832ebe20c 100644
--- a/source/blender/editors/gpencil/gpencil_edit.c
+++ b/source/blender/editors/gpencil/gpencil_edit.c
@@ -44,6 +44,7 @@
#include "BLT_translation.h"
+#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "DNA_screen_types.h"
#include "DNA_space_types.h"
@@ -58,21 +59,63 @@
#include "BKE_screen.h"
#include "UI_interface.h"
+#include "UI_resources.h"
#include "WM_api.h"
#include "WM_types.h"
#include "RNA_access.h"
#include "RNA_define.h"
+#include "RNA_enum_types.h"
#include "UI_view2d.h"
#include "ED_gpencil.h"
+#include "ED_object.h"
#include "ED_view3d.h"
#include "gpencil_intern.h"
/* ************************************************ */
+/* Stroke Edit Mode Management */
+
+static int gpencil_editmode_toggle_poll(bContext *C)
+{
+ return ED_gpencil_data_get_active(C) != NULL;
+}
+
+static int gpencil_editmode_toggle_exec(bContext *C, wmOperator *op)
+{
+ bGPdata *gpd = ED_gpencil_data_get_active(C);
+
+ if (gpd == NULL)
+ return OPERATOR_CANCELLED;
+
+ /* Just toggle editmode flag... */
+ gpd->flag ^= GP_DATA_STROKE_EDITMODE;
+
+ WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | ND_GPENCIL_EDITMODE, NULL);
+ WM_event_add_notifier(C, NC_SCENE | ND_MODE, NULL);
+
+ return OPERATOR_FINISHED;
+}
+
+void GPENCIL_OT_editmode_toggle(wmOperatorType *ot)
+{
+ /* identifiers */
+ ot->name = "Strokes Edit Mode Toggle";
+ ot->idname = "GPENCIL_OT_editmode_toggle";
+ ot->description = "Enter/Exit edit mode for Grease Pencil strokes";
+
+ /* callbacks */
+ ot->exec = gpencil_editmode_toggle_exec;
+ ot->poll = gpencil_editmode_toggle_poll;
+
+ /* flags */
+ ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
+}
+
+/* ************************************************ */
/* Stroke Editing Operators */
/* poll callback for all stroke editing operators */
@@ -233,7 +276,8 @@ void GPENCIL_OT_duplicate(wmOperatorType *ot)
*/
/* list of bGPDstroke instances */
-static ListBase gp_strokes_copypastebuf = {NULL, NULL};
+/* NOTE: is exposed within the editors/gpencil module so that other tools can use it too */
+ListBase gp_strokes_copypastebuf = {NULL, NULL};
/* Free copy/paste buffer data */
void ED_gpencil_strokes_copybuf_free(void)
@@ -339,7 +383,7 @@ static int gp_strokes_paste_exec(bContext *C, wmOperator *op)
BKE_report(op->reports, RPT_ERROR, "No Grease Pencil data");
return OPERATOR_CANCELLED;
}
- else if (gp_strokes_copypastebuf.first == NULL) {
+ else if (BLI_listbase_is_empty(&gp_strokes_copypastebuf)) {
BKE_report(op->reports, RPT_ERROR, "No strokes to paste, select and copy some points before trying again");
return OPERATOR_CANCELLED;
}
@@ -434,6 +478,110 @@ void GPENCIL_OT_paste(wmOperatorType *ot)
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
+/* ******************* Move To Layer ****************************** */
+
+static int gp_move_to_layer_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(evt))
+{
+ uiPopupMenu *pup;
+ uiLayout *layout;
+
+ /* call the menu, which will call this operator again, hence the canceled */
+ pup = UI_popup_menu_begin(C, op->type->name, ICON_NONE);
+ layout = UI_popup_menu_layout(pup);
+ uiItemsEnumO(layout, "GPENCIL_OT_move_to_layer", "layer");
+ UI_popup_menu_end(C, pup);
+
+ return OPERATOR_INTERFACE;
+}
+
+// FIXME: allow moving partial strokes
+static int gp_move_to_layer_exec(bContext *C, wmOperator *op)
+{
+ bGPdata *gpd = CTX_data_gpencil_data(C);
+ bGPDlayer *target_layer = NULL;
+ ListBase strokes = {NULL, NULL};
+ int layer_num = RNA_enum_get(op->ptr, "layer");
+
+ /* Get layer or create new one */
+ if (layer_num == -1) {
+ /* Create layer */
+ target_layer = gpencil_layer_addnew(gpd, DATA_("GP_Layer"), true);
+ }
+ else {
+ /* Try to get layer */
+ target_layer = BLI_findlink(&gpd->layers, layer_num);
+
+ if (target_layer == NULL) {
+ BKE_reportf(op->reports, RPT_ERROR, "There is no layer number %d", layer_num);
+ return OPERATOR_CANCELLED;
+ }
+ }
+
+ /* Extract all strokes to move to this layer
+ * NOTE: We need to do this in a two-pass system to avoid conflicts with strokes
+ * getting repeatedly moved
+ */
+ CTX_DATA_BEGIN(C, bGPDlayer *, gpl, editable_gpencil_layers)
+ {
+ bGPDframe *gpf = gpl->actframe;
+ bGPDstroke *gps, *gpsn;
+
+ /* skip if no frame with strokes, or if this is the layer we're moving strokes to */
+ if ((gpl == target_layer) || (gpf == NULL))
+ continue;
+
+ /* make copies of selected strokes, and deselect these once we're done */
+ for (gps = gpf->strokes.first; gps; gps = gpsn) {
+ gpsn = gps->next;
+
+ /* skip strokes that are invalid for current view */
+ if (ED_gpencil_stroke_can_use(C, gps) == false)
+ continue;
+
+ /* TODO: Don't just move entire strokes - instead, only copy the selected portions... */
+ if (gps->flag & GP_STROKE_SELECT) {
+ BLI_remlink(&gpf->strokes, gps);
+ BLI_addtail(&strokes, gps);
+ }
+ }
+ }
+ CTX_DATA_END;
+
+ /* Paste them all in one go */
+ if (strokes.first) {
+ Scene *scene = CTX_data_scene(C);
+ bGPDframe *gpf = gpencil_layer_getframe(target_layer, CFRA, true);
+
+ BLI_movelisttolist(&gpf->strokes, &strokes);
+ BLI_assert((strokes.first == strokes.last) && (strokes.first == NULL));
+ }
+
+ /* updates */
+ WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
+
+ return OPERATOR_FINISHED;
+}
+
+void GPENCIL_OT_move_to_layer(wmOperatorType *ot)
+{
+ /* identifiers */
+ ot->name = "Move Strokes to Layer";
+ ot->idname = "GPENCIL_OT_move_to_layer";
+ ot->description = "Move selected strokes to another layer"; // XXX: allow moving individual points too?
+
+ /* callbacks */
+ ot->invoke = gp_move_to_layer_invoke;
+ ot->exec = gp_move_to_layer_exec;
+ ot->poll = gp_stroke_edit_poll; // XXX?
+
+ /* flags */
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+
+ /* gp layer to use (dynamic enum) */
+ ot->prop = RNA_def_enum(ot->srna, "layer", DummyRNA_DEFAULT_items, 0, "Grease Pencil Layer", "");
+ RNA_def_enum_funcs(ot->prop, ED_gpencil_layers_with_new_enum_itemf);
+}
+
/* ******************* Delete Active Frame ************************ */
static int gp_actframe_delete_poll(bContext *C)
@@ -497,6 +645,7 @@ typedef enum eGP_DeleteMode {
GP_DELETEOP_FRAME = 2,
} eGP_DeleteMode;
+/* ----------------------------------- */
/* Delete selected strokes */
static int gp_delete_selected_strokes(bContext *C)
@@ -540,6 +689,8 @@ static int gp_delete_selected_strokes(bContext *C)
}
}
+/* ----------------------------------- */
+
/* Delete selected points but keep the stroke */
static int gp_dissolve_selected_points(bContext *C)
{
@@ -621,6 +772,124 @@ static int gp_dissolve_selected_points(bContext *C)
}
}
+/* ----------------------------------- */
+
+/* Temp data for storing information about an "island" of points
+ * that should be kept when splitting up a stroke. Used in:
+ * gp_stroke_delete_tagged_points()
+ */
+typedef struct tGPDeleteIsland {
+ int start_idx;
+ int end_idx;
+} tGPDeleteIsland;
+
+
+/* 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
+ */
+void gp_stroke_delete_tagged_points(bGPDframe *gpf, bGPDstroke *gps, bGPDstroke *next_stroke, int tag_flags)
+{
+ tGPDeleteIsland *islands = MEM_callocN(sizeof(tGPDeleteIsland) * (gps->totpoints + 1) / 2, "gp_point_islands");
+ bool in_island = false;
+ int num_islands = 0;
+
+ bGPDspoint *pt;
+ int i;
+
+ /* First Pass: Identify start/end of islands */
+ for (i = 0, pt = gps->points; 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];
+ bGPDstroke *new_stroke = MEM_dupallocN(gps);
+
+ /* Compute new buffer size (+ 1 needed as the endpoint index is "inclusive") */
+ new_stroke->totpoints = island->end_idx - island->start_idx + 1;
+ new_stroke->points = MEM_callocN(sizeof(bGPDspoint) * new_stroke->totpoints, "gp delete stroke fragment");
+
+ /* Copy over the relevant points */
+ memcpy(new_stroke->points, gps->points + island->start_idx, sizeof(bGPDspoint) * new_stroke->totpoints);
+
+
+ /* 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 remaing 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;
+ }
+ }
+
+ /* Add new stroke to the frame */
+ if (next_stroke) {
+ BLI_insertlinkbefore(&gpf->strokes, next_stroke, new_stroke);
+ }
+ else {
+ BLI_addtail(&gpf->strokes, new_stroke);
+ }
+ }
+ }
+
+ /* free islands */
+ MEM_freeN(islands);
+
+ /* Delete the old stroke */
+ MEM_freeN(gps->points);
+ BLI_freelinkN(&gpf->strokes, gps);
+}
+
+
/* Split selected strokes into segments, splitting on selected points */
static int gp_delete_selected_points(bContext *C)
{
@@ -644,89 +913,11 @@ static int gp_delete_selected_points(bContext *C)
if (gps->flag & GP_STROKE_SELECT) {
- bGPDspoint *pt;
- int i;
-
- /* The algorithm used here is as follows:
- * 1) We firstly identify the number of "islands" of non-selected 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
- */
- typedef struct tGPDeleteIsland {
- int start_idx;
- int end_idx;
- } tGPDeleteIsland;
-
- tGPDeleteIsland *islands = MEM_callocN(sizeof(tGPDeleteIsland) * (gps->totpoints + 1) / 2, "gp_point_islands");
- bool in_island = false;
- int num_islands = 0;
-
- /* First Pass: Identify start/end of islands */
- for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
- if (pt->flag & GP_SPOINT_SELECT) {
- /* 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;
-
- /* deselect old stroke, since it will be used as template for the new strokes */
- gps->flag &= ~GP_STROKE_SELECT;
-
- /* create each new stroke... */
- for (idx = 0; idx < num_islands; idx++) {
- tGPDeleteIsland *island = &islands[idx];
- bGPDstroke *new_stroke = MEM_dupallocN(gps);
-
- /* compute new buffer size (+ 1 needed as the endpoint index is "inclusive") */
- new_stroke->totpoints = island->end_idx - island->start_idx + 1;
- new_stroke->points = MEM_callocN(sizeof(bGPDspoint) * new_stroke->totpoints, "gp delete stroke fragment");
-
- /* copy over the relevant points */
- memcpy(new_stroke->points, gps->points + island->start_idx, sizeof(bGPDspoint) * new_stroke->totpoints);
-
- /* add new stroke to the frame */
- if (gpsn) {
- BLI_insertlinkbefore(&gpf->strokes, gpsn, new_stroke);
- }
- else {
- BLI_addtail(&gpf->strokes, new_stroke);
- }
- }
- }
-
- /* free islands */
- MEM_freeN(islands);
+ /* deselect old stroke, since it will be used as template for the new strokes */
+ gps->flag &= ~GP_STROKE_SELECT;
- /* Delete the old stroke */
- MEM_freeN(gps->points);
- BLI_freelinkN(&gpf->strokes, gps);
+ /* delete unwanted points by splitting stroke into several smaller ones */
+ gp_stroke_delete_tagged_points(gpf, gps, gpsn, GP_SPOINT_SELECT);
changed = true;
}
@@ -743,6 +934,7 @@ static int gp_delete_selected_points(bContext *C)
}
}
+/* ----------------------------------- */
static int gp_delete_exec(bContext *C, wmOperator *op)
{
@@ -812,4 +1004,190 @@ void GPENCIL_OT_dissolve(wmOperatorType *ot)
ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
}
+/* ****************** Snapping - Strokes <-> Cursor ************************ */
+
+/* Poll callback for snap operators */
+/* NOTE: For now, we only allow these in the 3D view, as other editors do not
+ * define a cursor or gridstep which can be used
+ */
+static int gp_snap_poll(bContext *C)
+{
+ bGPdata *gpd = CTX_data_gpencil_data(C);
+ ScrArea *sa = CTX_wm_area(C);
+
+ return (gpd != NULL) && ((sa != NULL) && (sa->spacetype == SPACE_VIEW3D));
+}
+
+/* --------------------------------- */
+
+static int gp_snap_to_grid(bContext *C, wmOperator *op)
+{
+ RegionView3D *rv3d = CTX_wm_region_data(C);
+ float gridf = rv3d->gridview;
+
+ CTX_DATA_BEGIN(C, bGPDstroke *, gps, editable_gpencil_strokes)
+ {
+ bGPDspoint *pt;
+ int i;
+
+ // TOOD: if entire stroke is selected, offset entire stroke by same amount?
+
+ for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
+ /* only if point is selected.. */
+ if (pt->flag & GP_SPOINT_SELECT) {
+ pt->x = gridf * floorf(0.5f + pt->x / gridf);
+ pt->y = gridf * floorf(0.5f + pt->y / gridf);
+ pt->z = gridf * floorf(0.5f + pt->z / gridf);
+ }
+ }
+ }
+ CTX_DATA_END;
+
+ WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
+ return OPERATOR_FINISHED;
+}
+
+void GPENCIL_OT_snap_to_grid(wmOperatorType *ot)
+{
+ /* identifiers */
+ ot->name = "Snap Selection to Grid";
+ ot->idname = "GPENCIL_OT_snap_to_grid";
+ ot->description = "Snap selected points to the nearest grid points";
+
+ /* callbacks */
+ ot->exec = gp_snap_to_grid;
+ ot->poll = gp_snap_poll;
+
+ /* flags */
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+}
+
+/* ------------------------------- */
+
+static int gp_snap_to_cursor(bContext *C, wmOperator *op)
+{
+ Scene *scene = CTX_data_scene(C);
+ View3D *v3d = CTX_wm_view3d(C);
+
+ const bool use_offset = RNA_boolean_get(op->ptr, "use_offset");
+ const float *cursor_global = ED_view3d_cursor3d_get(scene, v3d);
+
+ CTX_DATA_BEGIN(C, bGPDstroke *, gps, editable_gpencil_strokes)
+ {
+ bGPDspoint *pt;
+ int i;
+
+ /* only continue if this stroke is selected (editable doesn't guarantee this)... */
+ if ((gps->flag & GP_STROKE_SELECT) == 0)
+ continue;
+
+ if (use_offset) {
+ float offset[3];
+
+ /* compute offset from first point of stroke to cursor */
+ /* TODO: Allow using midpoint instead? */
+ sub_v3_v3v3(offset, cursor_global, &gps->points->x);
+
+ /* apply offset to all points in the stroke */
+ for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
+ add_v3_v3(&pt->x, offset);
+ }
+ }
+ else {
+ /* affect each selected point */
+ for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
+ if (pt->flag & GP_SPOINT_SELECT) {
+ copy_v3_v3(&pt->x, cursor_global);
+ }
+ }
+ }
+ }
+ CTX_DATA_END;
+
+ WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
+ return OPERATOR_FINISHED;
+}
+
+void GPENCIL_OT_snap_to_cursor(wmOperatorType *ot)
+{
+ /* identifiers */
+ ot->name = "Snap Selection to Cursor";
+ ot->idname = "GPENCIL_OT_snap_to_cursor";
+ ot->description = "Snap selected points/strokes to the cursor";
+
+ /* callbacks */
+ ot->exec = gp_snap_to_cursor;
+ ot->poll = gp_snap_poll;
+
+ /* flags */
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+
+ /* props */
+ ot->prop = RNA_def_boolean(ot->srna, "use_offset", true, "With Offset",
+ "Offset the entire stroke instead of selected points only");
+}
+
+/* ------------------------------- */
+
+static int gp_snap_cursor_to_sel(bContext *C, wmOperator *op)
+{
+ Scene *scene = CTX_data_scene(C);
+ View3D *v3d = CTX_wm_view3d(C);
+
+ float *cursor = ED_view3d_cursor3d_get(scene, v3d);
+ float centroid[3] = {0.0f};
+ float min[3], max[3];
+ size_t count = 0;
+
+ INIT_MINMAX(min, max);
+
+ /* calculate midpoints from selected points */
+ CTX_DATA_BEGIN(C, bGPDstroke *, gps, editable_gpencil_strokes)
+ {
+ bGPDspoint *pt;
+ int i;
+
+ /* only continue if this stroke is selected (editable doesn't guarantee this)... */
+ if ((gps->flag & GP_STROKE_SELECT) == 0)
+ continue;
+
+ for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
+ if (pt->flag & GP_SPOINT_SELECT) {
+ add_v3_v3(centroid, &pt->x);
+ minmax_v3v3_v3(min, max, &pt->x);
+ count++;
+ }
+ }
+ }
+ CTX_DATA_END;
+
+ if (v3d->around == V3D_AROUND_CENTER_MEAN) {
+ mul_v3_fl(centroid, 1.0f / (float)count);
+ copy_v3_v3(cursor, centroid);
+ }
+ else {
+ mid_v3_v3v3(cursor, min, max);
+ }
+
+
+ WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
+ return OPERATOR_FINISHED;
+}
+
+void GPENCIL_OT_snap_cursor_to_selected(wmOperatorType *ot)
+{
+ /* identifiers */
+ ot->name = "Snap Cursor to Selected Points";
+ ot->idname = "GPENCIL_OT_snap_cursor_to_selected";
+ ot->description = "Snap cursor to center of selected points";
+
+ /* callbacks */
+ ot->exec = gp_snap_cursor_to_sel;
+ ot->poll = gp_snap_poll;
+
+ /* flags */
+ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
+}
+
+
/* ************************************************ */
diff --git a/source/blender/editors/gpencil/gpencil_intern.h b/source/blender/editors/gpencil/gpencil_intern.h
index 9c0b6f785d1..d9a6441c00e 100644
--- a/source/blender/editors/gpencil/gpencil_intern.h
+++ b/source/blender/editors/gpencil/gpencil_intern.h
@@ -44,6 +44,10 @@ struct ARegion;
struct View2D;
struct wmOperatorType;
+struct PointerRNA;
+struct PropertyRNA;
+struct EnumPropertyItem;
+
/* ***************************************************** */
/* Internal API */
@@ -96,12 +100,37 @@ void gp_point_conversion_init(struct bContext *C, GP_SpaceConversion *r_gsc);
void gp_point_to_xy(GP_SpaceConversion *settings, struct bGPDstroke *gps, struct bGPDspoint *pt,
int *r_x, int *r_y);
+/**
+ * Convert a screenspace point to a 3D Grease Pencil coordinate.
+ *
+ * For use with editing tools where it is easier to perform the operations in 2D,
+ * and then later convert the transformed points back to 3D.
+ *
+ * \param screeN_co The screenspace 2D coordinates to convert to
+ * \param[out] r_out The resulting 3D coordinates of the input point
+ */
+bool gp_point_xy_to_3d(GP_SpaceConversion *gsc, struct Scene *scene, const float screen_co[2], float r_out[3]);
+
/* Poll Callbacks ------------------------------------ */
/* gpencil_utils.c */
int gp_add_poll(struct bContext *C);
int gp_active_layer_poll(struct bContext *C);
+/* Copy/Paste Buffer --------------------------------- */
+/* gpencil_edit.c */
+
+extern ListBase gp_strokes_copypastebuf;
+
+/* Stroke Editing ------------------------------------ */
+
+void gp_stroke_delete_tagged_points(bGPDframe *gpf, bGPDstroke *gps, bGPDstroke *next_stroke, int tag_flags);
+
+/* Layers Enums -------------------------------------- */
+
+struct EnumPropertyItem *ED_gpencil_layers_enum_itemf(struct bContext *C, struct PointerRNA *ptr, struct PropertyRNA *prop, bool *r_free);
+struct EnumPropertyItem *ED_gpencil_layers_with_new_enum_itemf(struct bContext *C, struct PointerRNA *ptr, struct PropertyRNA *prop, bool *r_free);
+
/* ***************************************************** */
/* Operator Defines */
@@ -119,6 +148,8 @@ typedef enum eGPencil_PaintModes {
/* stroke editing ----- */
+void GPENCIL_OT_editmode_toggle(struct wmOperatorType *ot);
+
void GPENCIL_OT_select(struct wmOperatorType *ot);
void GPENCIL_OT_select_all(struct wmOperatorType *ot);
void GPENCIL_OT_select_circle(struct wmOperatorType *ot);
@@ -135,6 +166,19 @@ void GPENCIL_OT_dissolve(struct wmOperatorType *ot);
void GPENCIL_OT_copy(struct wmOperatorType *ot);
void GPENCIL_OT_paste(struct wmOperatorType *ot);
+void GPENCIL_OT_move_to_layer(struct wmOperatorType *ot);
+void GPENCIL_OT_layer_change(struct wmOperatorType *ot);
+
+void GPENCIL_OT_snap_to_grid(struct wmOperatorType *ot);
+void GPENCIL_OT_snap_to_cursor(struct wmOperatorType *ot);
+void GPENCIL_OT_snap_cursor_to_selected(struct wmOperatorType *ot);
+void GPENCIL_OT_snap_cursor_to_center(struct wmOperatorType *ot);
+
+
+/* stroke sculpting -- */
+
+void GPENCIL_OT_brush_paint(struct wmOperatorType *ot);
+
/* buttons editing --- */
void GPENCIL_OT_data_add(struct wmOperatorType *ot);
@@ -148,6 +192,11 @@ void GPENCIL_OT_layer_duplicate(struct wmOperatorType *ot);
void GPENCIL_OT_hide(struct wmOperatorType *ot);
void GPENCIL_OT_reveal(struct wmOperatorType *ot);
+void GPENCIL_OT_lock_all(struct wmOperatorType *ot);
+void GPENCIL_OT_unlock_all(struct wmOperatorType *ot);
+
+void GPENCIL_OT_layer_isolate(struct wmOperatorType *ot);
+
void GPENCIL_OT_active_frame_delete(struct wmOperatorType *ot);
void GPENCIL_OT_convert(struct wmOperatorType *ot);
diff --git a/source/blender/editors/gpencil/gpencil_ops.c b/source/blender/editors/gpencil/gpencil_ops.c
index ab56565f4ca..25012ab6d64 100644
--- a/source/blender/editors/gpencil/gpencil_ops.c
+++ b/source/blender/editors/gpencil/gpencil_ops.c
@@ -79,11 +79,25 @@ static void ed_keymap_gpencil_general(wmKeyConfig *keyconf)
RNA_enum_set(kmi->ptr, "mode", GP_PAINTMODE_ERASER);
RNA_boolean_set(kmi->ptr, "wait_for_input", false);
+
+ /* Tablet Mappings for Drawing ------------------ */
+ /* For now, only support direct drawing using the eraser, as most users using a tablet
+ * may still want to use that as their primary pointing device!
+ */
+#if 0
+ kmi = WM_keymap_add_item(keymap, "GPENCIL_OT_draw", TABLET_STYLUS, KM_PRESS, 0, 0);
+ RNA_enum_set(kmi->ptr, "mode", GP_PAINTMODE_DRAW);
+ RNA_boolean_set(kmi->ptr, "wait_for_input", false);
+#endif
+
+ kmi = WM_keymap_add_item(keymap, "GPENCIL_OT_draw", TABLET_ERASER, KM_PRESS, 0, 0);
+ RNA_enum_set(kmi->ptr, "mode", GP_PAINTMODE_ERASER);
+ RNA_boolean_set(kmi->ptr, "wait_for_input", false);
+
/* Viewport Tools ------------------------------- */
/* Enter EditMode */
- kmi = WM_keymap_add_item(keymap, "WM_OT_context_toggle", TABKEY, KM_PRESS, 0, DKEY);
- RNA_string_set(kmi->ptr, "data_path", "gpencil_data.use_stroke_edit_mode");
+ WM_keymap_add_item(keymap, "GPENCIL_OT_editmode_toggle", TABKEY, KM_PRESS, 0, DKEY);
/* Pie Menu - For standard tools */
WM_keymap_add_menu_pie(keymap, "GPENCIL_PIE_tool_palette", QKEY, KM_PRESS, 0, DKEY);
@@ -111,8 +125,10 @@ static void ed_keymap_gpencil_editing(wmKeyConfig *keyconf)
/* ----------------------------------------------- */
/* Exit EditMode */
- kmi = WM_keymap_add_item(keymap, "WM_OT_context_toggle", TABKEY, KM_PRESS, 0, 0);
- RNA_string_set(kmi->ptr, "data_path", "gpencil_data.use_stroke_edit_mode");
+ WM_keymap_add_item(keymap, "GPENCIL_OT_editmode_toggle", TABKEY, KM_PRESS, 0, 0);
+
+ /* Pie Menu - For settings/tools easy access */
+ WM_keymap_add_menu_pie(keymap, "GPENCIL_PIE_sculpt", EKEY, KM_PRESS, 0, DKEY);
/* Brush Settings */
/* NOTE: We cannot expose these in the standard keymap, as they will interfere with regular hotkeys
@@ -185,6 +201,14 @@ static void ed_keymap_gpencil_editing(wmKeyConfig *keyconf)
WM_keymap_add_item(keymap, "GPENCIL_OT_copy", CKEY, KM_PRESS, KM_OSKEY, 0);
WM_keymap_add_item(keymap, "GPENCIL_OT_paste", VKEY, KM_PRESS, KM_OSKEY, 0);
#endif
+
+ /* snap */
+ WM_keymap_add_menu(keymap, "GPENCIL_MT_snap", SKEY, KM_PRESS, KM_SHIFT, 0);
+
+
+ /* convert to geometry */
+ WM_keymap_add_item(keymap, "GPENCIL_OT_convert", CKEY, KM_PRESS, KM_ALT, 0);
+
/* Show/Hide */
/* NOTE: These are available only in EditMode now, since they clash with general-purpose hotkeys */
@@ -196,35 +220,62 @@ static void ed_keymap_gpencil_editing(wmKeyConfig *keyconf)
kmi = WM_keymap_add_item(keymap, "GPENCIL_OT_hide", HKEY, KM_PRESS, KM_SHIFT, 0);
RNA_boolean_set(kmi->ptr, "unselected", true);
+ /* Isolate Layer */
+ WM_keymap_add_item(keymap, "GPENCIL_OT_layer_isolate", PADASTERKEY, KM_PRESS, 0, 0);
+
+ /* Move to Layer */
+ WM_keymap_add_item(keymap, "GPENCIL_OT_move_to_layer", MKEY, KM_PRESS, 0, 0);
+
+
+
+ /* Brush-Based Editing:
+ * EKEY + LMB = Single stroke, draw immediately
+ * + Other Modifiers (Ctrl/Shift) = Invert, Smooth, etc.
+ *
+ * For the modal version, use D+E -> Sculpt
+ */
+ kmi = WM_keymap_add_item(keymap, "GPENCIL_OT_brush_paint", LEFTMOUSE, KM_PRESS, 0, EKEY);
+ RNA_boolean_set(kmi->ptr, "wait_for_input", false);
+
+ kmi = WM_keymap_add_item(keymap, "GPENCIL_OT_brush_paint", LEFTMOUSE, KM_PRESS, KM_CTRL, EKEY);
+ RNA_boolean_set(kmi->ptr, "wait_for_input", false);
+ /*RNA_boolean_set(kmi->ptr, "use_invert", true);*/
+
+ kmi = WM_keymap_add_item(keymap, "GPENCIL_OT_brush_paint", LEFTMOUSE, KM_PRESS, KM_SHIFT, EKEY);
+ RNA_boolean_set(kmi->ptr, "wait_for_input", false);
+ /*RNA_boolean_set(kmi->ptr, "use_smooth", true);*/
+
+
+ /* Shift-FKEY = Sculpt Strength */
+ kmi = WM_keymap_add_item(keymap, "WM_OT_radial_control", FKEY, KM_PRESS, KM_SHIFT, 0);
+ RNA_string_set(kmi->ptr, "data_path_primary", "tool_settings.gpencil_sculpt.brush.strength");
+
+ /* Ctrl-FKEY = Sculpt Brush Size */
+ kmi = WM_keymap_add_item(keymap, "WM_OT_radial_control", FKEY, KM_PRESS, KM_CTRL, 0);
+ RNA_string_set(kmi->ptr, "data_path_primary", "tool_settings.gpencil_sculpt.brush.size");
+
+
+
/* Transform Tools */
kmi = WM_keymap_add_item(keymap, "TRANSFORM_OT_translate", GKEY, KM_PRESS, 0, 0);
- RNA_boolean_set(kmi->ptr, "gpencil_strokes", true);
kmi = WM_keymap_add_item(keymap, "TRANSFORM_OT_translate", EVT_TWEAK_S, KM_ANY, 0, 0);
- RNA_boolean_set(kmi->ptr, "gpencil_strokes", true);
kmi = WM_keymap_add_item(keymap, "TRANSFORM_OT_rotate", RKEY, KM_PRESS, 0, 0);
- RNA_boolean_set(kmi->ptr, "gpencil_strokes", true);
kmi = WM_keymap_add_item(keymap, "TRANSFORM_OT_resize", SKEY, KM_PRESS, 0, 0);
- RNA_boolean_set(kmi->ptr, "gpencil_strokes", true);
kmi = WM_keymap_add_item(keymap, "TRANSFORM_OT_mirror", MKEY, KM_PRESS, KM_CTRL, 0);
- RNA_boolean_set(kmi->ptr, "gpencil_strokes", true);
kmi = WM_keymap_add_item(keymap, "TRANSFORM_OT_bend", WKEY, KM_PRESS, KM_SHIFT, 0);
- RNA_boolean_set(kmi->ptr, "gpencil_strokes", true);
WM_keymap_add_item(keymap, "TRANSFORM_OT_tosphere", SKEY, KM_PRESS, KM_ALT | KM_SHIFT, 0);
- RNA_boolean_set(kmi->ptr, "gpencil_strokes", true);
WM_keymap_add_item(keymap, "TRANSFORM_OT_shear", SKEY, KM_PRESS, KM_ALT | KM_CTRL | KM_SHIFT, 0);
- RNA_boolean_set(kmi->ptr, "gpencil_strokes", true);
kmi = WM_keymap_add_item(keymap, "TRANSFORM_OT_transform", SKEY, KM_PRESS, KM_ALT, 0);
RNA_enum_set(kmi->ptr, "mode", TFM_GPENCIL_SHRINKFATTEN);
- RNA_boolean_set(kmi->ptr, "gpencil_strokes", true);
/* Proportional Editing */
ED_keymap_proportional_cycle(keyconf, keymap);
@@ -249,6 +300,8 @@ void ED_operatortypes_gpencil(void)
/* Editing (Strokes) ------------ */
+ WM_operatortype_append(GPENCIL_OT_editmode_toggle);
+
WM_operatortype_append(GPENCIL_OT_select);
WM_operatortype_append(GPENCIL_OT_select_all);
WM_operatortype_append(GPENCIL_OT_select_circle);
@@ -265,6 +318,15 @@ void ED_operatortypes_gpencil(void)
WM_operatortype_append(GPENCIL_OT_copy);
WM_operatortype_append(GPENCIL_OT_paste);
+ WM_operatortype_append(GPENCIL_OT_move_to_layer);
+ WM_operatortype_append(GPENCIL_OT_layer_change);
+
+ WM_operatortype_append(GPENCIL_OT_snap_to_grid);
+ WM_operatortype_append(GPENCIL_OT_snap_to_cursor);
+ WM_operatortype_append(GPENCIL_OT_snap_cursor_to_selected);
+
+ WM_operatortype_append(GPENCIL_OT_brush_paint);
+
/* Editing (Buttons) ------------ */
WM_operatortype_append(GPENCIL_OT_data_add);
@@ -277,6 +339,9 @@ void ED_operatortypes_gpencil(void)
WM_operatortype_append(GPENCIL_OT_hide);
WM_operatortype_append(GPENCIL_OT_reveal);
+ WM_operatortype_append(GPENCIL_OT_lock_all);
+ WM_operatortype_append(GPENCIL_OT_unlock_all);
+ WM_operatortype_append(GPENCIL_OT_layer_isolate);
WM_operatortype_append(GPENCIL_OT_active_frame_delete);
@@ -290,12 +355,14 @@ void ED_operatormacros_gpencil(void)
wmOperatorType *ot;
wmOperatorTypeMacro *otmacro;
+ /* Duplicate + Move = Interactively place newly duplicated strokes */
ot = WM_operatortype_append_macro("GPENCIL_OT_duplicate_move", "Duplicate Strokes",
"Make copies of the selected Grease Pencil strokes and move them",
OPTYPE_UNDO | OPTYPE_REGISTER);
WM_operatortype_macro_define(ot, "GPENCIL_OT_duplicate");
otmacro = WM_operatortype_macro_define(ot, "TRANSFORM_OT_translate");
RNA_boolean_set(otmacro->ptr, "gpencil_strokes", true);
+
}
/* ****************************************** */
diff --git a/source/blender/editors/gpencil/gpencil_paint.c b/source/blender/editors/gpencil/gpencil_paint.c
index df88da073ca..c5a92c4383d 100644
--- a/source/blender/editors/gpencil/gpencil_paint.c
+++ b/source/blender/editors/gpencil/gpencil_paint.c
@@ -77,7 +77,33 @@
/* ******************************************* */
/* 'Globals' and Defines */
-/* Temporary 'Stroke' Operation data */
+/* values for tGPsdata->status */
+typedef enum eGPencil_PaintStatus {
+ GP_STATUS_IDLING = 0, /* stroke isn't in progress yet */
+ GP_STATUS_PAINTING, /* a stroke is in progress */
+ GP_STATUS_ERROR, /* something wasn't correctly set up */
+ GP_STATUS_DONE /* painting done */
+} eGPencil_PaintStatus;
+
+/* Return flags for adding points to stroke buffer */
+typedef enum eGP_StrokeAdd_Result {
+ GP_STROKEADD_INVALID = -2, /* error occurred - insufficient info to do so */
+ GP_STROKEADD_OVERFLOW = -1, /* error occurred - cannot fit any more points */
+ GP_STROKEADD_NORMAL, /* point was successfully added */
+ GP_STROKEADD_FULL /* cannot add any more points to buffer */
+} eGP_StrokeAdd_Result;
+
+/* Runtime flags */
+typedef enum eGPencil_PaintFlags {
+ GP_PAINTFLAG_FIRSTRUN = (1 << 0), /* operator just started */
+ GP_PAINTFLAG_STROKEADDED = (1 << 1),
+ GP_PAINTFLAG_V3D_ERASER_DEPTH = (1 << 2)
+} eGPencil_PaintFlags;
+
+
+/* Temporary 'Stroke' Operation data
+ * "p" = op->customdata
+ */
typedef struct tGPsdata {
Scene *scene; /* current scene from context */
@@ -95,8 +121,13 @@ typedef struct tGPsdata {
bGPDlayer *gpl; /* layer we're working on */
bGPDframe *gpf; /* frame we're working on */
- short status; /* current status of painting */
- short paintmode; /* mode for painting */
+ char *align_flag; /* projection-mode flags (toolsettings - eGPencil_Placement_Flags) */
+
+ eGPencil_PaintStatus status; /* current status of painting */
+ eGPencil_PaintModes paintmode; /* mode for painting */
+ eGPencil_PaintFlags flags; /* flags that can get set during runtime (eGPencil_PaintFlags) */
+
+ short radius; /* radius of influence for eraser */
int mval[2]; /* current mouse-position */
int mvalo[2]; /* previous recorded mouse-position */
@@ -104,9 +135,6 @@ typedef struct tGPsdata {
float pressure; /* current stylus pressure */
float opressure; /* previous stylus pressure */
- short radius; /* radius of influence for eraser */
- short flags; /* flags that can get set during runtime */
-
/* These need to be doubles, as (at least under unix) they are in seconds since epoch,
* float (and its 7 digits precision) is definitively not enough here!
* double, with its 15 digits precision, ensures us millisecond precision for a few centuries at least.
@@ -124,29 +152,6 @@ typedef struct tGPsdata {
void *erasercursor; /* radial cursor data for drawing eraser */
} tGPsdata;
-/* values for tGPsdata->status */
-enum {
- GP_STATUS_IDLING = 0, /* stroke isn't in progress yet */
- GP_STATUS_PAINTING, /* a stroke is in progress */
- GP_STATUS_ERROR, /* something wasn't correctly set up */
- GP_STATUS_DONE /* painting done */
-};
-
-/* Return flags for adding points to stroke buffer */
-enum {
- GP_STROKEADD_INVALID = -2, /* error occurred - insufficient info to do so */
- GP_STROKEADD_OVERFLOW = -1, /* error occurred - cannot fit any more points */
- GP_STROKEADD_NORMAL, /* point was successfully added */
- GP_STROKEADD_FULL /* cannot add any more points to buffer */
-};
-
-/* Runtime flags */
-enum {
- GP_PAINTFLAG_FIRSTRUN = (1 << 0), /* operator just started */
- GP_PAINTFLAG_STROKEADDED = (1 << 1),
- GP_PAINTFLAG_V3D_ERASER_DEPTH = (1 << 2)
-};
-
/* ------ */
/* maximum sizes of gp-session buffer */
@@ -204,7 +209,7 @@ static int gpencil_draw_poll(bContext *C)
static bool gpencil_project_check(tGPsdata *p)
{
bGPdata *gpd = p->gpd;
- return ((gpd->sbuffer_sflag & GP_STROKE_3DSPACE) && (p->gpd->flag & (GP_DATA_DEPTH_VIEW | GP_DATA_DEPTH_STROKE)));
+ return ((gpd->sbuffer_sflag & GP_STROKE_3DSPACE) && (*p->align_flag & (GP_PROJECT_DEPTH_VIEW | GP_PROJECT_DEPTH_STROKE)));
}
/* ******************************************* */
@@ -736,112 +741,6 @@ static void gp_stroke_newfrombuffer(tGPsdata *p)
/* --- 'Eraser' for 'Paint' Tool ------ */
-/* eraser tool - remove segment from stroke/split stroke (after lasso inside) */
-static short gp_stroke_eraser_splitdel(bGPDframe *gpf, bGPDstroke *gps, int i)
-{
- bGPDspoint *pt_tmp = gps->points;
- bGPDstroke *gsn = NULL;
-
- /* if stroke only had two points, get rid of stroke */
- if (gps->totpoints == 2) {
- /* free stroke points, then stroke */
- MEM_freeN(pt_tmp);
- BLI_freelinkN(&gpf->strokes, gps);
-
- /* nothing left in stroke, so stop */
- return 1;
- }
-
- /* if last segment, just remove segment from the stroke */
- else if (i == gps->totpoints - 2) {
- /* allocate new points array, and assign most of the old stroke there */
- gps->totpoints--;
- gps->points = MEM_mallocN(sizeof(bGPDspoint) * gps->totpoints, "gp_stroke_points");
- memcpy(gps->points, pt_tmp, sizeof(bGPDspoint) * gps->totpoints);
-
- /* free temp buffer */
- MEM_freeN(pt_tmp);
-
- /* nothing left in stroke, so stop */
- return 1;
- }
-
- /* if first segment, just remove segment from the stroke */
- else if (i == 0) {
- /* allocate new points array, and assign most of the old stroke there */
- gps->totpoints--;
- gps->points = MEM_mallocN(sizeof(bGPDspoint) * gps->totpoints, "gp_stroke_points");
- memcpy(gps->points, pt_tmp + 1, sizeof(bGPDspoint) * gps->totpoints);
-
- /* We must adjust timings!
- * Each point's timing data is a delta from stroke's inittime, so as we erase the first
- * point 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 second point...
- */
- {
- bGPDspoint *pts;
- float delta = pt_tmp[1].time;
- int j;
-
- gps->inittime += (double)delta;
-
- pts = gps->points;
- for (j = 0; j < gps->totpoints; j++, pts++) {
- pts->time -= delta;
- }
- }
-
- /* free temp buffer */
- MEM_freeN(pt_tmp);
-
- /* no break here, as there might still be stuff to remove in this stroke */
- return 0;
- }
-
- /* segment occurs in 'middle' of stroke, so split */
- else {
- /* duplicate stroke, and assign 'later' data to that stroke */
- gsn = MEM_dupallocN(gps);
- gsn->prev = gsn->next = NULL;
- BLI_insertlinkafter(&gpf->strokes, gps, gsn);
-
- gsn->totpoints = gps->totpoints - i;
- gsn->points = MEM_mallocN(sizeof(bGPDspoint) * gsn->totpoints, "gp_stroke_points");
- memcpy(gsn->points, pt_tmp + i, sizeof(bGPDspoint) * gsn->totpoints);
-
- /* We must adjust timings of this new stroke!
- * Each point's timing data is a delta from stroke's inittime, so as we erase the first
- * point of the stroke, we have to offset this inittime and all remaing points' delta values.
- * This way we get a new stroke with exactly the same timing as if user had started drawing from
- * the second point...
- */
- {
- bGPDspoint *pts;
- float delta = pt_tmp[i].time;
- int j;
-
- gsn->inittime += (double)delta;
-
- pts = gsn->points;
- for (j = 0; j < gsn->totpoints; j++, pts++) {
- pts->time -= delta;
- }
- }
-
- /* adjust existing stroke */
- gps->totpoints = i;
- gps->points = MEM_mallocN(sizeof(bGPDspoint) * gps->totpoints, "gp_stroke_points");
- memcpy(gps->points, pt_tmp, sizeof(bGPDspoint) * gps->totpoints);
-
- /* free temp buffer */
- MEM_freeN(pt_tmp);
-
- /* nothing left in stroke, so stop */
- return 1;
- }
-}
-
/* which which point is infront (result should only be used for comparison) */
static float view3d_point_depth(const RegionView3D *rv3d, const float co[3])
{
@@ -853,6 +752,7 @@ static float view3d_point_depth(const RegionView3D *rv3d, const float co[3])
}
}
+/* only erase stroke points that are visible */
static bool gp_stroke_eraser_is_occluded(tGPsdata *p, const bGPDspoint *pt, const int x, const int y)
{
if ((p->sa->spacetype == SPACE_VIEW3D) &&
@@ -874,15 +774,33 @@ static bool gp_stroke_eraser_is_occluded(tGPsdata *p, const bGPDspoint *pt, cons
return false;
}
+/* apply a falloff effect to brush strength, based on distance */
+static float gp_stroke_eraser_calc_influence(tGPsdata *p, const int mval[2], const int radius, const int co[2])
+{
+ /* Linear Falloff... */
+ float distance = (float)len_v2v2_int(mval, co);
+ float fac;
+
+ CLAMP(distance, 0.0f, (float)radius);
+ fac = 1.0f - (distance / (float)radius);
+
+ /* Control this further using pen pressure */
+ fac *= p->pressure;
+
+ /* Return influence factor computed here */
+ return fac;
+}
/* eraser tool - evaluation per stroke */
/* TODO: this could really do with some optimization (KD-Tree/BVH?) */
static void gp_stroke_eraser_dostroke(tGPsdata *p,
+ bGPDlayer *gpl, bGPDframe *gpf, bGPDstroke *gps,
const int mval[2], const int mvalo[2],
- short rad, const rcti *rect, bGPDframe *gpf, bGPDstroke *gps)
+ const int radius, const rcti *rect)
{
bGPDspoint *pt1, *pt2;
- int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
+ int pc1[2] = {0};
+ int pc2[2] = {0};
int i;
if (gps->totpoints == 0) {
@@ -892,56 +810,101 @@ static void gp_stroke_eraser_dostroke(tGPsdata *p,
BLI_freelinkN(&gpf->strokes, gps);
}
else if (gps->totpoints == 1) {
- gp_point_to_xy(&p->gsc, gps, gps->points, &x0, &y0);
+ gp_point_to_xy(&p->gsc, gps, gps->points, &pc1[0], &pc1[1]);
/* do boundbox check first */
- if ((!ELEM(V2D_IS_CLIPPED, x0, y0)) && BLI_rcti_isect_pt(rect, x0, y0)) {
+ if ((!ELEM(V2D_IS_CLIPPED, pc1[0], pc1[1])) && BLI_rcti_isect_pt(rect, pc1[0], pc1[1])) {
/* only check if point is inside */
- if (((x0 - mval[0]) * (x0 - mval[0]) + (y0 - mval[1]) * (y0 - mval[1])) <= rad * rad) {
+ if (len_v2v2_int(mval, pc1) <= radius) {
/* free stroke */
+ // XXX: pressure sensitive eraser should apply here too?
MEM_freeN(gps->points);
BLI_freelinkN(&gpf->strokes, gps);
}
}
}
else {
- /* loop over the points in the stroke, checking for intersections
- * - an intersection will require the stroke to be split
+ /* Pressure threshold at which stroke should be culled: Calculated as pressure value
+ * below which we would have invisible strokes
+ */
+ const float cull_thresh = (gpl->thickness) ? 1.0f / ((float)gpl->thickness) : 1.0f;
+
+ /* Amount to decrease the pressure of each point with each stroke */
+ // TODO: Fetch from toolsettings, or compute based on thickness instead?
+ const float strength = 0.1f;
+
+ /* Perform culling? */
+ bool do_cull = false;
+
+
+ /* Clear Tags
+ *
+ * Note: It's better this way, as we are sure that
+ * we don't miss anything, though things will be
+ * slightly slower as a result
+ */
+ for (i = 0; i < gps->totpoints; i++) {
+ bGPDspoint *pt = &gps->points[i];
+ pt->flag &= ~GP_SPOINT_TAG;
+ }
+
+ /* First Pass: Loop over the points in the stroke
+ * 1) Thin out parts of the stroke under the brush
+ * 2) Tag "too thin" parts for removal (in second pass)
*/
for (i = 0; (i + 1) < gps->totpoints; i++) {
/* get points to work with */
pt1 = gps->points + i;
pt2 = gps->points + i + 1;
- gp_point_to_xy(&p->gsc, gps, pt1, &x0, &y0);
- gp_point_to_xy(&p->gsc, gps, pt2, &x1, &y1);
+ gp_point_to_xy(&p->gsc, gps, pt1, &pc1[0], &pc1[1]);
+ gp_point_to_xy(&p->gsc, gps, pt2, &pc2[0], &pc2[1]);
- /* check that point segment of the boundbox of the eraser stroke */
- if (((!ELEM(V2D_IS_CLIPPED, x0, y0)) && BLI_rcti_isect_pt(rect, x0, y0)) ||
- ((!ELEM(V2D_IS_CLIPPED, x1, y1)) && BLI_rcti_isect_pt(rect, x1, y1)))
+ /* Check that point segment of the boundbox of the eraser 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
+ /* Check if point segment of stroke had anything to do with
* eraser region (either within stroke painted, or on its lines)
* - this assumes that linewidth is irrelevant
*/
- if (gp_stroke_inside_circle(mval, mvalo, rad, x0, y0, x1, y1)) {
- if ((gp_stroke_eraser_is_occluded(p, pt1, x0, y0) == false) ||
- (gp_stroke_eraser_is_occluded(p, pt2, x1, y1) == false))
+ if (gp_stroke_inside_circle(mval, mvalo, radius, pc1[0], pc1[1], pc2[0], pc2[1])) {
+ if ((gp_stroke_eraser_is_occluded(p, pt1, pc1[0], pc1[1]) == false) ||
+ (gp_stroke_eraser_is_occluded(p, pt2, pc2[0], pc2[1]) == false))
{
- /* if function returns true, break this loop (as no more point to check) */
- if (gp_stroke_eraser_splitdel(gpf, gps, i))
- break;
+ /* Point is affected: */
+ /* 1) Adjust thickness
+ * - Influence of eraser falls off with distance from the middle of the eraser
+ * - Second point gets less influence, as it might get hit again in the next segment
+ */
+ pt1->pressure -= gp_stroke_eraser_calc_influence(p, mval, radius, pc1) * strength;
+ pt2->pressure -= gp_stroke_eraser_calc_influence(p, mval, radius, pc2) * strength / 2.0f;
+
+ /* 2) Tag any point with overly low influence for removal in the next pass */
+ if (pt1->pressure < cull_thresh) {
+ pt1->flag |= GP_SPOINT_TAG;
+ do_cull = true;
+ }
+ if (pt2->pressure < cull_thresh) {
+ pt2->flag |= GP_SPOINT_TAG;
+ do_cull = true;
+ }
}
}
}
}
+
+ /* Second Pass: Remove any points that are tagged */
+ if (do_cull) {
+ gp_stroke_delete_tagged_points(gpf, gps, gps->next, GP_SPOINT_TAG);
+ }
}
}
/* erase strokes which fall under the eraser strokes */
static void gp_stroke_doeraser(tGPsdata *p)
{
- bGPDframe *gpf = p->gpf;
+ bGPDlayer *gpl;
bGPDstroke *gps, *gpn;
rcti rect;
@@ -960,10 +923,32 @@ static void gp_stroke_doeraser(tGPsdata *p)
}
}
- /* loop over strokes, checking segments for intersections */
- for (gps = gpf->strokes.first; gps; gps = gpn) {
- gpn = gps->next;
- gp_stroke_eraser_dostroke(p, p->mval, p->mvalo, p->radius, &rect, gpf, gps);
+ /* loop over all layers too, since while it's easy to restrict editing to
+ * only a subset of layers, it is harder to perform the same erase operation
+ * on multiple layers...
+ */
+ for (gpl = p->gpd->layers.first; gpl; gpl = gpl->next) {
+ bGPDframe *gpf = gpl->actframe;
+
+ /* only affect layer if it's editable (and visible) */
+ if (gpl->flag & (GP_LAYER_HIDE | GP_LAYER_LOCKED)) {
+ continue;
+ }
+ else if (gpf == NULL) {
+ continue;
+ }
+
+ /* loop over strokes, checking segments for intersections */
+ for (gps = gpf->strokes.first; gps; gps = gpn) {
+ gpn = gps->next;
+
+ /* Not all strokes in the datablock may be valid in the current editor/context
+ * (e.g. 2D space strokes in the 3D view, if the same datablock is shared)
+ */
+ if (ED_gpencil_stroke_can_use_direct(p->sa, gps)) {
+ gp_stroke_eraser_dostroke(p, gpl, gpf, gps, p->mval, p->mvalo, p->radius, &rect);
+ }
+ }
}
}
@@ -1001,6 +986,7 @@ static bool gp_session_initdata(bContext *C, tGPsdata *p)
bGPdata **gpd_ptr = NULL;
ScrArea *curarea = CTX_wm_area(C);
ARegion *ar = CTX_wm_region(C);
+ ToolSettings *ts = CTX_data_tool_settings(C);
/* make sure the active view (at the starting time) is a 3d-view */
if (curarea == NULL) {
@@ -1030,6 +1016,7 @@ static bool gp_session_initdata(bContext *C, tGPsdata *p)
/* CAUTION: If this is the "toolbar", then this will change on the first stroke */
p->sa = curarea;
p->ar = ar;
+ p->align_flag = &ts->gpencil_v3d_align;
if (ar->regiondata == NULL) {
p->status = GP_STATUS_ERROR;
@@ -1047,6 +1034,7 @@ static bool gp_session_initdata(bContext *C, tGPsdata *p)
p->sa = curarea;
p->ar = ar;
p->v2d = &ar->v2d;
+ p->align_flag = &ts->gpencil_v2d_align;
break;
}
case SPACE_SEQ:
@@ -1057,6 +1045,7 @@ static bool gp_session_initdata(bContext *C, tGPsdata *p)
p->sa = curarea;
p->ar = ar;
p->v2d = &ar->v2d;
+ p->align_flag = &ts->gpencil_seq_align;
/* check that gpencil data is allowed to be drawn */
if (sseq->mainb == SEQ_DRAW_SEQUENCE) {
@@ -1075,6 +1064,7 @@ static bool gp_session_initdata(bContext *C, tGPsdata *p)
p->sa = curarea;
p->ar = ar;
p->v2d = &ar->v2d;
+ p->align_flag = &ts->gpencil_ima_align;
break;
}
case SPACE_CLIP:
@@ -1091,6 +1081,7 @@ static bool gp_session_initdata(bContext *C, tGPsdata *p)
p->sa = curarea;
p->ar = ar;
p->v2d = &ar->v2d;
+ p->align_flag = &ts->gpencil_v2d_align;
invert_m4_m4(p->imat, sc->unistabmat);
@@ -1167,6 +1158,12 @@ static tGPsdata *gp_session_initpaint(bContext *C)
gp_session_initdata(C, p);
+ /* radius for eraser circle is defined in userprefs now */
+ /* NOTE: we do this here, so that if we exit immediately,
+ * erase size won't get lost
+ */
+ p->radius = U.gp_eraser;
+
/* return context data for running paint operator */
return p;
}
@@ -1194,8 +1191,10 @@ static void gp_session_cleanup(tGPsdata *p)
}
/* init new stroke */
-static void gp_paint_initstroke(tGPsdata *p, short paintmode)
+static void gp_paint_initstroke(tGPsdata *p, eGPencil_PaintModes paintmode)
{
+ Scene *scene = p->scene;
+
/* get active layer (or add a new one if non-existent) */
p->gpl = gpencil_layer_getactive(p->gpd);
if (p->gpl == NULL) {
@@ -1212,15 +1211,61 @@ static void gp_paint_initstroke(tGPsdata *p, short paintmode)
}
/* get active frame (add a new one if not matching frame) */
- p->gpf = gpencil_layer_getframe(p->gpl, p->scene->r.cfra, 1);
- if (p->gpf == NULL) {
- p->status = GP_STATUS_ERROR;
- if (G.debug & G_DEBUG)
- printf("Error: No frame created (gpencil_paint_init)\n");
- return;
+ if (paintmode == GP_PAINTMODE_ERASER) {
+ /* Eraser mode:
+ * 1) Add new frames to all frames that we might touch,
+ * 2) Ensure that p->gpf refers to the frame used for the active layer
+ * (to avoid problems with other tools which expect it to exist)
+ */
+ bGPDlayer *gpl;
+ for (gpl = p->gpd->layers.first; gpl; gpl = gpl->next) {
+ /* Skip if layer not editable */
+ if (gpl->flag & (GP_LAYER_HIDE | GP_LAYER_LOCKED))
+ continue;
+
+ /* Add a new frame if needed (and based off the active frame,
+ * as we need some existing strokes to erase)
+ */
+ gpl->actframe = gpencil_layer_getframe(gpl, CFRA, GP_GETFRAME_ADD_COPY);
+
+ /* XXX: we omit GP_FRAME_PAINT here for now,
+ * as it is only really useful for doing
+ * paintbuffer drawing
+ */
+ }
+
+ /* Ensure this gets set... */
+ p->gpf = p->gpl->actframe;
+
+ if (p->gpf == NULL) {
+ p->status = GP_STATUS_ERROR;
+ //if (G.debug & G_DEBUG)
+ printf("Error: No frame created (gpencil_paint_init)\n");
+ return;
+ }
+ }
+ else {
+ /* Drawing Modes - Add a new frame if needed on the active layer */
+ ToolSettings *ts = p->scene->toolsettings;
+ short add_frame_mode;
+
+ if (ts->gpencil_flags & GP_TOOL_FLAG_RETAIN_LAST)
+ add_frame_mode = GP_GETFRAME_ADD_COPY;
+ else
+ add_frame_mode = GP_GETFRAME_ADD_NEW;
+
+ p->gpf = gpencil_layer_getframe(p->gpl, CFRA, add_frame_mode);
+
+ if (p->gpf == NULL) {
+ p->status = GP_STATUS_ERROR;
+ if (G.debug & G_DEBUG)
+ printf("Error: No frame created (gpencil_paint_init)\n");
+ return;
+ }
+ else {
+ p->gpf->flag |= GP_FRAME_PAINT;
+ }
}
- else
- p->gpf->flag |= GP_FRAME_PAINT;
/* set 'eraser' for this stroke if using eraser */
p->paintmode = paintmode;
@@ -1251,7 +1296,7 @@ static void gp_paint_initstroke(tGPsdata *p, short paintmode)
/* when drawing in the camera view, in 2D space, set the subrect */
p->subrect = NULL;
- if (!(p->gpd->flag & GP_DATA_VIEWALIGN)) {
+ if ((*p->align_flag & GP_PROJECT_VIEWSPACE) == 0) {
if (p->sa->spacetype == SPACE_VIEW3D) {
View3D *v3d = p->sa->spacedata.first;
RegionView3D *rv3d = p->ar->regiondata;
@@ -1279,7 +1324,7 @@ static void gp_paint_initstroke(tGPsdata *p, short paintmode)
/* check if points will need to be made in view-aligned space */
- if (p->gpd->flag & GP_DATA_VIEWALIGN) {
+ if (*p->align_flag & GP_PROJECT_VIEWSPACE) {
switch (p->sa->spacetype) {
case SPACE_VIEW3D:
{
@@ -1308,7 +1353,7 @@ static void gp_paint_initstroke(tGPsdata *p, short paintmode)
if (ELEM(NULL, sima, sima->image)) {
/* make strokes be drawn in screen space */
p->gpd->sbuffer_sflag &= ~GP_STROKE_2DSPACE;
- p->gpd->flag &= ~GP_DATA_VIEWALIGN;
+ *(p->align_flag) &= ~GP_PROJECT_VIEWSPACE;
}
else {
p->gpd->sbuffer_sflag |= GP_STROKE_2DSPACE;
@@ -1417,6 +1462,17 @@ static void gpencil_draw_toggle_eraser_cursor(bContext *C, tGPsdata *p, short en
}
}
+/* Check if tablet eraser is being used (when processing events) */
+static bool gpencil_is_tablet_eraser_active(const wmEvent *event)
+{
+ if (event->tablet_data) {
+ const wmTabletData *wmtab = event->tablet_data;
+ return (wmtab->Active == EVT_TABLET_ERASER);
+ }
+
+ return false;
+}
+
/* ------------------------------- */
@@ -1467,7 +1523,7 @@ static void gpencil_draw_cancel(bContext *C, wmOperator *op)
static int gpencil_draw_init(bContext *C, wmOperator *op)
{
tGPsdata *p;
- int paintmode = RNA_enum_get(op->ptr, "mode");
+ eGPencil_PaintModes paintmode = RNA_enum_get(op->ptr, "mode");
/* check context */
p = op->customdata = gp_session_initpaint(C);
@@ -1484,9 +1540,6 @@ static int gpencil_draw_init(bContext *C, wmOperator *op)
return 0;
}
- /* radius for eraser circle is defined in userprefs now */
- p->radius = U.gp_eraser;
-
/* everything is now setup ok */
return 1;
}
@@ -1529,6 +1582,10 @@ static void gpencil_draw_status_indicators(tGPsdata *p)
ED_area_headerprint(p->sa, IFACE_("Grease Pencil Freehand Session: Hold and drag LMB to draw | "
"ESC/Enter to end"));
break;
+ case GP_PAINTMODE_DRAW_POLY:
+ ED_area_headerprint(p->sa, IFACE_("Grease Pencil Poly Session: LMB click to place next stroke vertex | "
+ "ESC/Enter to end"));
+ break;
default: /* unhandled future cases */
ED_area_headerprint(p->sa, IFACE_("Grease Pencil Session: ESC/Enter to end"));
@@ -1622,9 +1679,6 @@ static void gpencil_draw_apply_event(wmOperator *op, const wmEvent *event)
tablet = (wmtab->Active != EVT_TABLET_NONE);
p->pressure = wmtab->Pressure;
-
- /* if (wmtab->Active == EVT_TABLET_ERASER) */
- /* TODO... this should get caught by the keymaps which call drawing in the first place */
}
else
p->pressure = 1.0f;
@@ -1819,7 +1873,6 @@ static tGPsdata *gpencil_stroke_begin(bContext *C, wmOperator *op)
/* we may need to set up paint env again if we're resuming */
/* XXX: watch it with the paintmode! in future,
* it'd be nice to allow changing paint-mode when in sketching-sessions */
- /* XXX: with tablet events, we may event want to check for eraser here, for nicer tablet support */
if (gp_session_initdata(C, p))
gp_paint_initstroke(p, p->paintmode);
@@ -2008,14 +2061,14 @@ static int gpencil_draw_modal(bContext *C, wmOperator *op, const wmEvent *event)
/* Switch paintmode (temporarily if need be) based on which button was used
* NOTE: This is to make it more convenient to erase strokes when using drawing sessions
*/
- if (event->type == LEFTMOUSE) {
- /* restore drawmode to default */
- p->paintmode = RNA_enum_get(op->ptr, "mode");
- }
- else if (event->type == RIGHTMOUSE) {
+ if ((event->type == RIGHTMOUSE) || gpencil_is_tablet_eraser_active(event)) {
/* turn on eraser */
p->paintmode = GP_PAINTMODE_ERASER;
}
+ else if (event->type == LEFTMOUSE) {
+ /* restore drawmode to default */
+ p->paintmode = RNA_enum_get(op->ptr, "mode");
+ }
gpencil_draw_toggle_eraser_cursor(C, p, p->paintmode == GP_PAINTMODE_ERASER);
diff --git a/source/blender/editors/gpencil/gpencil_select.c b/source/blender/editors/gpencil/gpencil_select.c
index 0dd91019a8c..83a1f2458eb 100644
--- a/source/blender/editors/gpencil/gpencil_select.c
+++ b/source/blender/editors/gpencil/gpencil_select.c
@@ -67,10 +67,15 @@
static int gpencil_select_poll(bContext *C)
{
bGPdata *gpd = ED_gpencil_data_get_active(C);
- bGPDlayer *gpl = gpencil_layer_getactive(gpd);
- /* only if there's an active layer with an active frame */
- return (gpl && gpl->actframe);
+ /* we just need some visible strokes, and to be in editmode */
+ if ((gpd) && (gpd->flag & GP_DATA_STROKE_EDITMODE)) {
+ /* TODO: include a check for visible strokes? */
+ if (gpd->layers.first)
+ return true;
+ }
+
+ return false;
}
/* ********************************************** */
diff --git a/source/blender/editors/gpencil/gpencil_utils.c b/source/blender/editors/gpencil/gpencil_utils.c
index 0d7aac7f48f..36508751d08 100644
--- a/source/blender/editors/gpencil/gpencil_utils.c
+++ b/source/blender/editors/gpencil/gpencil_utils.c
@@ -51,7 +51,9 @@
#include "RNA_access.h"
#include "RNA_define.h"
+#include "RNA_enum_types.h"
+#include "UI_resources.h"
#include "UI_view2d.h"
#include "ED_gpencil.h"
@@ -76,8 +78,6 @@ bGPdata **ED_gpencil_data_get_pointers_direct(ID *screen_id, Scene *scene, ScrAr
switch (sa->spacetype) {
case SPACE_VIEW3D: /* 3D-View */
- case SPACE_TIME: /* Timeline - XXX: this is a hack to get it to show GP keyframes for 3D view */
- case SPACE_ACTION: /* DepeSheet - XXX: this is a hack to get the keyframe jump operator to take GP Keyframes into account */
{
BLI_assert(scene && ELEM(scene->toolsettings->gpencil_src,
GP_TOOL_SOURCE_SCENE, GP_TOOL_SOURCE_OBJECT));
@@ -212,6 +212,45 @@ bGPdata *ED_gpencil_data_get_active_v3d(Scene *scene, View3D *v3d)
}
/* ******************************************************** */
+/* Keyframe Indicator Checks */
+
+/* Check whether there's an active GP keyframe on the current frame */
+bool ED_gpencil_has_keyframe_v3d(Scene *scene, Object *ob, int cfra)
+{
+ /* just check both for now... */
+ // XXX: this could get confusing (e.g. if only on the object, but other places don't show this)
+ if (scene->gpd) {
+ bGPDlayer *gpl = gpencil_layer_getactive(scene->gpd);
+ if (gpl) {
+ if (gpl->actframe) {
+ // XXX: assumes that frame has been fetched already
+ return (gpl->actframe->framenum == cfra);
+ }
+ else {
+ /* XXX: disabled as could be too much of a penalty */
+ /* return BKE_gpencil_layer_find_frame(gpl, cfra); */
+ }
+ }
+ }
+
+ if (ob && ob->gpd) {
+ bGPDlayer *gpl = gpencil_layer_getactive(ob->gpd);
+ if (gpl) {
+ if (gpl->actframe) {
+ // XXX: assumes that frame has been fetched already
+ return (gpl->actframe->framenum == cfra);
+ }
+ else {
+ /* XXX: disabled as could be too much of a penalty */
+ /* return BKE_gpencil_layer_find_frame(gpl, cfra); */
+ }
+ }
+ }
+
+ return false;
+}
+
+/* ******************************************************** */
/* Poll Callbacks */
/* poll callback for adding data/layers - special */
@@ -231,6 +270,92 @@ int gp_active_layer_poll(bContext *C)
}
/* ******************************************************** */
+/* Dynamic Enums of GP Layers */
+/* NOTE: These include an option to create a new layer and use that... */
+
+/* Just existing layers */
+EnumPropertyItem *ED_gpencil_layers_enum_itemf(bContext *C, PointerRNA *UNUSED(ptr), PropertyRNA *UNUSED(prop), bool *r_free)
+{
+ bGPdata *gpd = CTX_data_gpencil_data(C);
+ bGPDlayer *gpl;
+ EnumPropertyItem *item = NULL, item_tmp = {0};
+ int totitem = 0;
+ int i = 0;
+
+ if (ELEM(NULL, C, gpd)) {
+ return DummyRNA_DEFAULT_items;
+ }
+
+ /* Existing layers */
+ for (gpl = gpd->layers.first; gpl; gpl = gpl->next, i++) {
+ item_tmp.identifier = gpl->info;
+ item_tmp.name = gpl->info;
+ item_tmp.value = i;
+
+ if (gpl->flag & GP_LAYER_ACTIVE)
+ item_tmp.icon = ICON_GREASEPENCIL;
+ else
+ item_tmp.icon = ICON_NONE;
+
+ RNA_enum_item_add(&item, &totitem, &item_tmp);
+ }
+
+ RNA_enum_item_end(&item, &totitem);
+ *r_free = true;
+
+ return item;
+}
+
+/* Existing + Option to add/use new layer */
+EnumPropertyItem *ED_gpencil_layers_with_new_enum_itemf(bContext *C, PointerRNA *UNUSED(ptr), PropertyRNA *UNUSED(prop), bool *r_free)
+{
+ bGPdata *gpd = CTX_data_gpencil_data(C);
+ bGPDlayer *gpl;
+ EnumPropertyItem *item = NULL, item_tmp = {0};
+ int totitem = 0;
+ int i = 0;
+
+ if (ELEM(NULL, C, gpd)) {
+ return DummyRNA_DEFAULT_items;
+ }
+
+ /* Create new layer */
+ /* TODO: have some way of specifying that we don't want this? */
+ {
+ /* active Keying Set */
+ item_tmp.identifier = "__CREATE__";
+ item_tmp.name = "New Layer";
+ item_tmp.value = -1;
+ item_tmp.icon = ICON_ZOOMIN;
+ RNA_enum_item_add(&item, &totitem, &item_tmp);
+
+ /* separator */
+ RNA_enum_item_add_separator(&item, &totitem);
+ }
+
+ /* Existing layers */
+ for (gpl = gpd->layers.first, i = 0; gpl; gpl = gpl->next, i++) {
+ item_tmp.identifier = gpl->info;
+ item_tmp.name = gpl->info;
+ item_tmp.value = i;
+
+ if (gpl->flag & GP_LAYER_ACTIVE)
+ item_tmp.icon = ICON_GREASEPENCIL;
+ else
+ item_tmp.icon = ICON_NONE;
+
+ RNA_enum_item_add(&item, &totitem, &item_tmp);
+ }
+
+ RNA_enum_item_end(&item, &totitem);
+ *r_free = true;
+
+ return item;
+}
+
+
+
+/* ******************************************************** */
/* Brush Tool Core */
/* Check if part of stroke occurs within last segment drawn by eraser */
@@ -372,4 +497,37 @@ void gp_point_to_xy(GP_SpaceConversion *gsc, bGPDstroke *gps, bGPDspoint *pt,
}
}
+/* Project screenspace coordinates to 3D-space
+ * NOTE: We include this as a utility function, since the standard method
+ * involves quite a few steps, which are invariably always the same
+ * for all GPencil operations. So, it's nicer to just centralise these.
+ * WARNING: Assumes that it is getting called in a 3D view only
+ */
+bool gp_point_xy_to_3d(GP_SpaceConversion *gsc, Scene *scene, const float screen_co[2], float r_out[3])
+{
+ View3D *v3d = gsc->sa->spacedata.first;
+ RegionView3D *rv3d = gsc->ar->regiondata;
+ float *rvec = ED_view3d_cursor3d_get(scene, v3d);
+ float ref[3] = {rvec[0], rvec[1], rvec[2]};
+ float zfac = ED_view3d_calc_zfac(rv3d, rvec, NULL);
+
+ float mval_f[2], mval_prj[2];
+ float dvec[3];
+
+ copy_v2_v2(mval_f, screen_co);
+
+ if (ED_view3d_project_float_global(gsc->ar, ref, mval_prj, V3D_PROJ_TEST_NOP) == V3D_PROJ_RET_OK) {
+ sub_v2_v2v2(mval_f, mval_prj, mval_f);
+ ED_view3d_win_to_delta(gsc->ar, mval_f, dvec, zfac);
+ sub_v3_v3v3(r_out, rvec, dvec);
+
+ return true;
+ }
+ else {
+ zero_v3(r_out);
+
+ return false;
+ }
+}
+
/* ******************************************************** */