text: /*
   * 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) 2008, Blender Foundation
   * This is a new part of Blender
   * Operators for editing Grease Pencil strokes
   */
  
  /** \file
   * \ingroup edgpencil
   */
  
  #include <math.h>
  #include <stddef.h>
  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>
  
  #include "MEM_guardedalloc.h"
  
  #include "BLI_blenlib.h"
  #include "BLI_ghash.h"
  #include "BLI_lasso_2d.h"
  #include "BLI_math.h"
  #include "BLI_string.h"
  #include "BLI_utildefines.h"
  
  #include "BLT_translation.h"
  
  #include "DNA_gpencil_modifier_types.h"
  #include "DNA_gpencil_types.h"
  #include "DNA_meshdata_types.h"
  #include "DNA_object_types.h"
  #include "DNA_scene_types.h"
  #include "DNA_screen_types.h"
  #include "DNA_space_types.h"
  #include "DNA_view3d_types.h"
  
  #include "BKE_brush.h"
  #include "BKE_context.h"
  #include "BKE_global.h"
  #include "BKE_gpencil.h"
  #include "BKE_gpencil_geom.h"
  #include "BKE_layer.h"
  #include "BKE_lib_id.h"
  #include "BKE_library.h"
  #include "BKE_main.h"
  #include "BKE_material.h"
  #include "BKE_object.h"
  #include "BKE_paint.h"
  #include "BKE_report.h"
  #include "BKE_scene.h"
  #include "BKE_workspace.h"
  
  #include "UI_interface.h"
  #include "UI_resources.h"
  
  #include "WM_api.h"
  #include "WM_message.h"
  #include "WM_toolsystem.h"
  #include "WM_types.h"
  
  #include "RNA_access.h"
  #include "RNA_define.h"
  #include "RNA_enum_types.h"
  
  #include "UI_view2d.h"
  
  #include "ED_armature.h"
  #include "ED_gpencil.h"
  #include "ED_object.h"
  #include "ED_outliner.h"
  #include "ED_screen.h"
  #include "ED_select_utils.h"
  #include "ED_space_api.h"
  #include "ED_transform_snap_object_context.h"
  #include "ED_view3d.h"
  
  #include "DEG_depsgraph.h"
  #include "DEG_depsgraph_build.h"
  #include "DEG_depsgraph_query.h"
  
  #include "gpencil_intern.h"
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Edit Mode Management
   * \{ */
  
  static void gpencil_flip_stroke(bGPDstroke *gps);
  
  /* poll callback for all stroke editing operators */
  static bool gpencil_stroke_edit_poll(bContext *C)
  {
    /* edit only supported with grease pencil objects */
    Object *ob = CTX_data_active_object(C);
    if ((ob == NULL) || (ob->type != OB_GPENCIL)) {
      return false;
    }
  
    /* NOTE: this is a bit slower, but is the most accurate... */
    return CTX_DATA_COUNT(C, editable_gpencil_strokes) != 0;
  }
  
  /* poll callback to verify edit mode in 3D view only */
  static bool gpencil_strokes_edit3d_poll(bContext *C)
  {
    /* edit only supported with grease pencil objects */
    Object *ob = CTX_data_active_object(C);
    if ((ob == NULL) || (ob->type != OB_GPENCIL)) {
      return false;
    }
  
    /* 2 Requirements:
     * - 1) Editable GP data
     * - 2) 3D View only
     */
    return (gpencil_stroke_edit_poll(C) && ED_operator_view3d_active(C));
  }
  
  static bool gpencil_editmode_toggle_poll(bContext *C)
  {
    /* edit only supported with grease pencil objects */
    Object *ob = CTX_data_active_object(C);
    if ((ob == NULL) || (ob->type != OB_GPENCIL)) {
      return false;
    }
  
    /* if using gpencil object, use this gpd */
    if (ob->type == OB_GPENCIL) {
      return ob->data != NULL;
    }
  
    return ED_gpencil_data_get_active(C) != NULL;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Toggle Edit Mode Operator
   * \{ */
  
  static int gpencil_editmode_toggle_exec(bContext *C, wmOperator *op)
  {
    const int back = RNA_boolean_get(op->ptr, "back");
  
    struct wmMsgBus *mbus = CTX_wm_message_bus(C);
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    bool is_object = false;
    short mode;
    /* if using a gpencil object, use this datablock */
    Object *ob = CTX_data_active_object(C);
    if ((ob) && (ob->type == OB_GPENCIL)) {
      gpd = ob->data;
      is_object = true;
    }
  
    if (gpd == NULL) {
      BKE_report(op->reports, RPT_ERROR, "No active GP data");
      return OPERATOR_CANCELLED;
    }
  
    /* Just toggle editmode flag... */
    gpd->flag ^= GP_DATA_STROKE_EDITMODE;
    /* recalculate parent matrix */
    if (gpd->flag & GP_DATA_STROKE_EDITMODE) {
      Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
      ED_gpencil_reset_layers_parent(depsgraph, ob, gpd);
    }
    /* set mode */
    if (gpd->flag & GP_DATA_STROKE_EDITMODE) {
      mode = OB_MODE_EDIT_GPENCIL;
    }
    else {
      mode = OB_MODE_OBJECT;
    }
  
    if (is_object) {
      /* try to back previous mode */
      if ((ob->restore_mode) && ((gpd->flag & GP_DATA_STROKE_EDITMODE) == 0) && (back == 1)) {
        mode = ob->restore_mode;
      }
      ob->restore_mode = ob->mode;
      ob->mode = mode;
    }
  
    /* setup other modes */
    ED_gpencil_setup_modes(C, gpd, mode);
    /* set cache as dirty */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
  
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA, NULL);
    WM_event_add_notifier(C, NC_GPENCIL | ND_GPENCIL_EDITMODE, NULL);
    WM_event_add_notifier(C, NC_SCENE | ND_MODE, NULL);
  
    if (is_object) {
      WM_msg_publish_rna_prop(mbus, &ob->id, ob, Object, mode);
    }
    if (G.background == false) {
      WM_toolsystem_update_from_context_view3d(C);
    }
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_editmode_toggle(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* 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;
  
    /* properties */
    prop = RNA_def_boolean(
        ot->srna, "back", 0, "Return to Previous Mode", "Return to previous mode");
    RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Toggle Select Mode Operator
   * \{ */
  
  /* set select mode */
  static bool gpencil_selectmode_toggle_poll(bContext *C)
  {
    /* edit only supported with grease pencil objects */
    Object *ob = CTX_data_active_object(C);
    if ((ob == NULL) || (ob->type != OB_GPENCIL) || (ob->mode != OB_MODE_EDIT_GPENCIL)) {
      return false;
    }
  
    return ED_operator_view3d_active(C);
  }
  
  static int gpencil_selectmode_toggle_exec(bContext *C, wmOperator *op)
  {
    Scene *scene = CTX_data_scene(C);
    ToolSettings *ts = CTX_data_tool_settings(C);
    Object *ob = CTX_data_active_object(C);
    const int mode = RNA_int_get(op->ptr, "mode");
    bool changed = false;
  
    if (ts->gpencil_selectmode_edit == mode) {
      return OPERATOR_FINISHED;
    }
  
    /* Just set mode */
    ts->gpencil_selectmode_edit = mode;
  
    /* If the mode is Stroke, extend selection. */
    if ((ob) && (ts->gpencil_selectmode_edit == GP_SELECTMODE_STROKE)) {
      bGPdata *gpd = (bGPdata *)ob->data;
      /* Extend selection to all points in all selected strokes. */
      CTX_DATA_BEGIN (C, bGPDstroke *, gps, editable_gpencil_strokes) {
        if ((gps->flag & GP_STROKE_SELECT) && (gps->totpoints > 1)) {
          changed = true;
          bGPDspoint *pt;
          for (int i = 0; i < gps->totpoints; i++) {
            pt = &gps->points[i];
            pt->flag |= GP_SPOINT_SELECT;
          }
        }
      }
      CTX_DATA_END;
      if (changed) {
        DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
      }
    }
  
    WM_event_add_notifier(C, NC_GPENCIL | NA_SELECTED, NULL);
    WM_main_add_notifier(NC_SCENE | ND_TOOLSETTINGS, NULL);
    DEG_id_tag_update(&scene->id, ID_RECALC_COPY_ON_WRITE);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_selectmode_toggle(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Select Mode Toggle";
    ot->idname = "GPENCIL_OT_selectmode_toggle";
    ot->description = "Set selection mode for Grease Pencil strokes";
  
    /* callbacks */
    ot->exec = gpencil_selectmode_toggle_exec;
    ot->poll = gpencil_selectmode_toggle_poll;
  
    /* flags */
    ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
  
    /* properties */
    prop = RNA_def_int(ot->srna, "mode", 0, 0, 2, "Select mode", "Select mode", 0, 2);
    RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Toggle Stroke Paint Mode Operator
   * \{ */
  
  static bool gpencil_paintmode_toggle_poll(bContext *C)
  {
    /* if using gpencil object, use this gpd */
    Object *ob = CTX_data_active_object(C);
    if ((ob) && (ob->type == OB_GPENCIL)) {
      return ob->data != NULL;
    }
    return ED_gpencil_data_get_active(C) != NULL;
  }
  
  static int gpencil_paintmode_toggle_exec(bContext *C, wmOperator *op)
  {
    const bool back = RNA_boolean_get(op->ptr, "back");
  
    struct wmMsgBus *mbus = CTX_wm_message_bus(C);
    Main *bmain = CTX_data_main(C);
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    ToolSettings *ts = CTX_data_tool_settings(C);
  
    bool is_object = false;
    short mode;
    /* if using a gpencil object, use this datablock */
    Object *ob = CTX_data_active_object(C);
    if ((ob) && (ob->type == OB_GPENCIL)) {
      gpd = ob->data;
      is_object = true;
    }
  
    if (gpd == NULL) {
      return OPERATOR_CANCELLED;
    }
  
    /* Just toggle paintmode flag... */
    gpd->flag ^= GP_DATA_STROKE_PAINTMODE;
    /* set mode */
    if (gpd->flag & GP_DATA_STROKE_PAINTMODE) {
      mode = OB_MODE_PAINT_GPENCIL;
    }
    else {
      mode = OB_MODE_OBJECT;
    }
  
    if (is_object) {
      /* try to back previous mode */
      if ((ob->restore_mode) && ((gpd->flag & GP_DATA_STROKE_PAINTMODE) == 0) && (back == 1)) {
        mode = ob->restore_mode;
      }
      ob->restore_mode = ob->mode;
      ob->mode = mode;
    }
  
    if (mode == OB_MODE_PAINT_GPENCIL) {
      /* Be sure we have brushes and Paint settings.
       * Need Draw and Vertex (used for Tint). */
      BKE_paint_ensure(ts, (Paint **)&ts->gp_paint);
      BKE_paint_ensure(ts, (Paint **)&ts->gp_vertexpaint);
  
      BKE_brush_gpencil_paint_presets(bmain, ts, false);
  
      /* Ensure Palette by default. */
      BKE_gpencil_palette_ensure(bmain, CTX_data_scene(C));
  
      Paint *paint = &ts->gp_paint->paint;
      /* if not exist, create a new one */
      if ((paint->brush == NULL) || (paint->brush->gpencil_settings == NULL)) {
        BKE_brush_gpencil_paint_presets(bmain, ts, true);
      }
      BKE_paint_toolslots_brush_validate(bmain, &ts->gp_paint->paint);
    }
  
    /* setup other modes */
    ED_gpencil_setup_modes(C, gpd, mode);
    /* set cache as dirty */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
  
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | ND_GPENCIL_EDITMODE, NULL);
    WM_event_add_notifier(C, NC_SCENE | ND_MODE, NULL);
  
    if (is_object) {
      WM_msg_publish_rna_prop(mbus, &ob->id, ob, Object, mode);
    }
    if (G.background == false) {
      WM_toolsystem_update_from_context_view3d(C);
    }
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_paintmode_toggle(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Strokes Paint Mode Toggle";
    ot->idname = "GPENCIL_OT_paintmode_toggle";
    ot->description = "Enter/Exit paint mode for Grease Pencil strokes";
  
    /* callbacks */
    ot->exec = gpencil_paintmode_toggle_exec;
    ot->poll = gpencil_paintmode_toggle_poll;
  
    /* flags */
    ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
  
    /* properties */
    prop = RNA_def_boolean(
        ot->srna, "back", 0, "Return to Previous Mode", "Return to previous mode");
    RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Toggle Stroke Sculpt Mode Operator
   * \{ */
  
  static bool gpencil_sculptmode_toggle_poll(bContext *C)
  {
    /* if using gpencil object, use this gpd */
    Object *ob = CTX_data_active_object(C);
    if ((ob) && (ob->type == OB_GPENCIL)) {
      return ob->data != NULL;
    }
    return ED_gpencil_data_get_active(C) != NULL;
  }
  
  static int gpencil_sculptmode_toggle_exec(bContext *C, wmOperator *op)
  {
    Main *bmain = CTX_data_main(C);
    ToolSettings *ts = CTX_data_tool_settings(C);
  
    const bool back = RNA_boolean_get(op->ptr, "back");
  
    struct wmMsgBus *mbus = CTX_wm_message_bus(C);
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    bool is_object = false;
    short mode;
    /* if using a gpencil object, use this datablock */
    Object *ob = CTX_data_active_object(C);
    if ((ob) && (ob->type == OB_GPENCIL)) {
      gpd = ob->data;
      is_object = true;
    }
  
    if (gpd == NULL) {
      return OPERATOR_CANCELLED;
    }
  
    /* Just toggle sculptmode flag... */
    gpd->flag ^= GP_DATA_STROKE_SCULPTMODE;
    /* set mode */
    if (gpd->flag & GP_DATA_STROKE_SCULPTMODE) {
      mode = OB_MODE_SCULPT_GPENCIL;
    }
    else {
      mode = OB_MODE_OBJECT;
    }
  
    if (is_object) {
      /* try to back previous mode */
      if ((ob->restore_mode) && ((gpd->flag & GP_DATA_STROKE_SCULPTMODE) == 0) && (back == 1)) {
        mode = ob->restore_mode;
      }
      ob->restore_mode = ob->mode;
      ob->mode = mode;
    }
  
    if (mode == OB_MODE_SCULPT_GPENCIL) {
      /* Be sure we have brushes. */
      BKE_paint_ensure(ts, (Paint **)&ts->gp_sculptpaint);
  
      const bool reset_mode = (ts->gp_sculptpaint->paint.brush == NULL);
      BKE_brush_gpencil_sculpt_presets(bmain, ts, reset_mode);
  
      BKE_paint_toolslots_brush_validate(bmain, &ts->gp_sculptpaint->paint);
    }
  
    /* setup other modes */
    ED_gpencil_setup_modes(C, gpd, mode);
    /* set cache as dirty */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
  
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | ND_GPENCIL_EDITMODE, NULL);
    WM_event_add_notifier(C, NC_SCENE | ND_MODE, NULL);
  
    if (is_object) {
      WM_msg_publish_rna_prop(mbus, &ob->id, ob, Object, mode);
    }
    if (G.background == false) {
      WM_toolsystem_update_from_context_view3d(C);
    }
  
    return OPERATOR_FINISHED;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Toggle Weight Paint Mode Operator
   * \{ */
  
  void GPENCIL_OT_sculptmode_toggle(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Strokes Sculpt Mode Toggle";
    ot->idname = "GPENCIL_OT_sculptmode_toggle";
    ot->description = "Enter/Exit sculpt mode for Grease Pencil strokes";
  
    /* callbacks */
    ot->exec = gpencil_sculptmode_toggle_exec;
    ot->poll = gpencil_sculptmode_toggle_poll;
  
    /* flags */
    ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
  
    /* properties */
    prop = RNA_def_boolean(
        ot->srna, "back", 0, "Return to Previous Mode", "Return to previous mode");
    RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
  }
  
  /* Stroke Weight Paint Mode Management */
  
  static bool gpencil_weightmode_toggle_poll(bContext *C)
  {
    /* if using gpencil object, use this gpd */
    Object *ob = CTX_data_active_object(C);
    if ((ob) && (ob->type == OB_GPENCIL)) {
      return ob->data != NULL;
    }
    return ED_gpencil_data_get_active(C) != NULL;
  }
  
  static int gpencil_weightmode_toggle_exec(bContext *C, wmOperator *op)
  {
    Main *bmain = CTX_data_main(C);
    ToolSettings *ts = CTX_data_tool_settings(C);
  
    const bool back = RNA_boolean_get(op->ptr, "back");
  
    struct wmMsgBus *mbus = CTX_wm_message_bus(C);
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    bool is_object = false;
    short mode;
    /* if using a gpencil object, use this datablock */
    Object *ob = CTX_data_active_object(C);
    if ((ob) && (ob->type == OB_GPENCIL)) {
      gpd = ob->data;
      is_object = true;
    }
    const int mode_flag = OB_MODE_WEIGHT_GPENCIL;
    const bool is_mode_set = (ob->mode & mode_flag) != 0;
  
    if (gpd == NULL) {
      return OPERATOR_CANCELLED;
    }
  
    /* Just toggle weightmode flag... */
    gpd->flag ^= GP_DATA_STROKE_WEIGHTMODE;
    /* set mode */
    if (gpd->flag & GP_DATA_STROKE_WEIGHTMODE) {
      mode = OB_MODE_WEIGHT_GPENCIL;
    }
    else {
      mode = OB_MODE_OBJECT;
    }
  
    if (is_object) {
      /* try to back previous mode */
      if ((ob->restore_mode) && ((gpd->flag & GP_DATA_STROKE_WEIGHTMODE) == 0) && (back == 1)) {
        mode = ob->restore_mode;
      }
      ob->restore_mode = ob->mode;
      ob->mode = mode;
  
      /* Prepare armature posemode. */
      ED_object_posemode_set_for_weight_paint(C, bmain, ob, is_mode_set);
    }
  
    if (mode == OB_MODE_WEIGHT_GPENCIL) {
      /* Be sure we have brushes. */
      BKE_paint_ensure(ts, (Paint **)&ts->gp_weightpaint);
  
      const bool reset_mode = (ts->gp_weightpaint->paint.brush == NULL);
      BKE_brush_gpencil_weight_presets(bmain, ts, reset_mode);
  
      BKE_paint_toolslots_brush_validate(bmain, &ts->gp_weightpaint->paint);
    }
  
    /* setup other modes */
    ED_gpencil_setup_modes(C, gpd, mode);
    /* set cache as dirty */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
  
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | ND_GPENCIL_EDITMODE, NULL);
    WM_event_add_notifier(C, NC_SCENE | ND_MODE, NULL);
  
    if (is_object) {
      WM_msg_publish_rna_prop(mbus, &ob->id, ob, Object, mode);
    }
    if (G.background == false) {
      WM_toolsystem_update_from_context_view3d(C);
    }
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_weightmode_toggle(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Strokes Weight Mode Toggle";
    ot->idname = "GPENCIL_OT_weightmode_toggle";
    ot->description = "Enter/Exit weight paint mode for Grease Pencil strokes";
  
    /* callbacks */
    ot->exec = gpencil_weightmode_toggle_exec;
    ot->poll = gpencil_weightmode_toggle_poll;
  
    /* flags */
    ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
  
    /* properties */
    prop = RNA_def_boolean(
        ot->srna, "back", 0, "Return to Previous Mode", "Return to previous mode");
    RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Toggle Vertex Paint Mode Operator
   * \{ */
  
  static bool gpencil_vertexmode_toggle_poll(bContext *C)
  {
    /* if using gpencil object, use this gpd */
    Object *ob = CTX_data_active_object(C);
    if ((ob) && (ob->type == OB_GPENCIL)) {
      return ob->data != NULL;
    }
    return ED_gpencil_data_get_active(C) != NULL;
  }
  static int gpencil_vertexmode_toggle_exec(bContext *C, wmOperator *op)
  {
    const bool back = RNA_boolean_get(op->ptr, "back");
  
    struct wmMsgBus *mbus = CTX_wm_message_bus(C);
    Main *bmain = CTX_data_main(C);
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    ToolSettings *ts = CTX_data_tool_settings(C);
  
    bool is_object = false;
    short mode;
    /* if using a gpencil object, use this datablock */
    Object *ob = CTX_data_active_object(C);
    if ((ob) && (ob->type == OB_GPENCIL)) {
      gpd = ob->data;
      is_object = true;
    }
  
    if (gpd == NULL) {
      return OPERATOR_CANCELLED;
    }
  
    /* Just toggle paintmode flag... */
    gpd->flag ^= GP_DATA_STROKE_VERTEXMODE;
    /* set mode */
    if (gpd->flag & GP_DATA_STROKE_VERTEXMODE) {
      mode = OB_MODE_VERTEX_GPENCIL;
    }
    else {
      mode = OB_MODE_OBJECT;
    }
  
    if (is_object) {
      /* try to back previous mode */
      if ((ob->restore_mode) && ((gpd->flag & GP_DATA_STROKE_VERTEXMODE) == 0) && (back == 1)) {
        mode = ob->restore_mode;
      }
      ob->restore_mode = ob->mode;
      ob->mode = mode;
    }
  
    if (mode == OB_MODE_VERTEX_GPENCIL) {
      /* Be sure we have brushes. */
      BKE_paint_ensure(ts, (Paint **)&ts->gp_vertexpaint);
  
      const bool reset_mode = (ts->gp_vertexpaint->paint.brush == NULL);
      BKE_brush_gpencil_vertex_presets(bmain, ts, reset_mode);
  
      BKE_paint_toolslots_brush_validate(bmain, &ts->gp_vertexpaint->paint);
  
      /* Ensure Palette by default. */
      BKE_gpencil_palette_ensure(bmain, CTX_data_scene(C));
    }
  
    /* setup other modes */
    ED_gpencil_setup_modes(C, gpd, mode);
    /* set cache as dirty */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
  
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | ND_GPENCIL_EDITMODE, NULL);
    WM_event_add_notifier(C, NC_SCENE | ND_MODE, NULL);
  
    if (is_object) {
      WM_msg_publish_rna_prop(mbus, &ob->id, ob, Object, mode);
    }
    if (G.background == false) {
      WM_toolsystem_update_from_context_view3d(C);
    }
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_vertexmode_toggle(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Strokes Vertex Mode Toggle";
    ot->idname = "GPENCIL_OT_vertexmode_toggle";
    ot->description = "Enter/Exit vertex paint mode for Grease Pencil strokes";
  
    /* callbacks */
    ot->exec = gpencil_vertexmode_toggle_exec;
    ot->poll = gpencil_vertexmode_toggle_poll;
  
    /* flags */
    ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
  
    /* properties */
    prop = RNA_def_boolean(
        ot->srna, "back", 0, "Return to Previous Mode", "Return to previous mode");
    RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Hide Selection Toggle Operator
   * \{ */
  
  static int gpencil_hideselect_toggle_exec(bContext *C, wmOperator *UNUSED(op))
  {
    View3D *v3d = CTX_wm_view3d(C);
    if (v3d == NULL) {
      return OPERATOR_CANCELLED;
    }
  
    /* Just toggle alpha... */
    if (v3d->vertex_opacity > 0.0f) {
      v3d->vertex_opacity = 0.0f;
    }
    else {
      v3d->vertex_opacity = 1.0f;
    }
  
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA, NULL);
    WM_event_add_notifier(C, NC_GPENCIL | ND_GPENCIL_EDITMODE, NULL);
    WM_event_add_notifier(C, NC_SCENE | ND_MODE, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_selection_opacity_toggle(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Hide Selected";
    ot->idname = "GPENCIL_OT_selection_opacity_toggle";
    ot->description = "Hide/Unhide selected points for Grease Pencil strokes setting alpha factor";
  
    /* callbacks */
    ot->exec = gpencil_hideselect_toggle_exec;
    ot->poll = gpencil_stroke_edit_poll;
  
    /* flags */
    ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Duplicate Selected Strokes Operator
   * \{ */
  
  /* Make copies of selected point segments in a selected stroke */
  static void gpencil_duplicate_points(const bGPDstroke *gps,
                                       ListBase *new_strokes,
                                       const char *layername)
  {
    bGPDspoint *pt;
    int i;
  
    int start_idx = -1;
  
    /* Step through the original stroke's points:
     * - We accumulate selected points (from start_idx to current index)
     *   and then convert that to a new stroke
     */
    for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
      /* searching for start, are waiting for end? */
      if (start_idx == -1) {
        /* is this the first selected point for a new island? */
        if (pt->flag & GP_SPOINT_SELECT) {
          start_idx = i;
        }
      }
      else {
        size_t len = 0;
  
        /* is this the end of current island yet?
         * 1) Point i-1 was the last one that was selected
         * 2) Point i is the last in the array
         */
        if ((pt->flag & GP_SPOINT_SELECT) == 0) {
          len = i - start_idx;
        }
        else if (i == gps->totpoints - 1) {
          len = i - start_idx + 1;
        }
  
        /* make copies of the relevant data */
        if (len) {
          bGPDstroke *gpsd;
  
          /* make a stupid copy first of the entire stroke (to get the flags too) */
          gpsd = BKE_gpencil_stroke_duplicate((bGPDstroke *)gps, false);
  
          /* saves original layer name */
          BLI_strncpy(gpsd->runtime.tmp_layerinfo, layername, sizeof(gpsd->runtime.tmp_layerinfo));
  
          /* now, make a new points array, and copy of the relevant parts */
          gpsd->points = MEM_mallocN(sizeof(bGPDspoint) * len, "gps stroke points copy");
          memcpy(gpsd->points, gps->points + start_idx, sizeof(bGPDspoint) * len);
          gpsd->totpoints = len;
  
          if (gps->dvert != NULL) {
            gpsd->dvert = MEM_mallocN(sizeof(MDeformVert) * len, "gps stroke weights copy");
            memcpy(gpsd->dvert, gps->dvert + start_idx, sizeof(MDeformVert) * len);
  
            /* Copy weights */
            int e = start_idx;
            for (int j = 0; j < gpsd->totpoints; j++) {
              MDeformVert *dvert_dst = &gps->dvert[e];
              MDeformVert *dvert_src = &gps->dvert[j];
              dvert_dst->dw = MEM_dupallocN(dvert_src->dw);
              e++;
            }
          }
  
          BKE_gpencil_stroke_geometry_update(gpsd);
  
          /* add to temp buffer */
          gpsd->next = gpsd->prev = NULL;
  
          BLI_addtail(new_strokes, gpsd);
  
          /* cleanup + reset for next */
          start_idx = -1;
        }
      }
    }
  }
  
  static int gpencil_duplicate_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
  
    if (gpd == NULL) {
      BKE_report(op->reports, RPT_ERROR, "No Grease Pencil data");
      return OPERATOR_CANCELLED;
    }
  
    if (GPENCIL_MULTIEDIT_SESSIONS_ON(gpd)) {
      BKE_report(op->reports, RPT_ERROR, "Operator not supported in multiframe edition");
      return OPERATOR_CANCELLED;
    }
  
    /* for each visible (and editable) layer's selected strokes,
     * copy the strokes into a temporary buffer, then append
     * once all done
     */
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      ListBase new_strokes = {NULL, NULL};
      bGPDframe *gpf = gpl->actframe;
      bGPDstroke *gps;
  
      if (gpf == NULL) {
        continue;
      }
  
      /* make copies of selected strokes, and deselect these once we're done */
      for (gps = gpf->strokes.first; gps; gps = gps->next) {
        /* skip strokes that are invalid for current view */
        if (ED_gpencil_stroke_can_use(C, gps) == false) {
          continue;
        }
  
        if (gps->flag & GP_STROKE_SELECT) {
          if (gps->totpoints == 1) {
            /* Special Case: If there's just a single point in this stroke... */
            bGPDstroke *gpsd;
  
            /* make direct copies of the stroke and its points */
            gpsd = BKE_gpencil_stroke_duplicate(gps, true);
  
            BLI_strncpy(gpsd->runtime.tmp_layerinfo, gpl->info, sizeof(gpsd->runtime.tmp_layerinfo));
  
            /* Initialize triangle information. */
            BKE_gpencil_stroke_geometry_update(gpsd);
  
            /* add to temp buffer */
            gpsd->next = gpsd->prev = NULL;
            BLI_addtail(&new_strokes, gpsd);
          }
          else {
            /* delegate to a helper, as there's too much to fit in here (for copying subsets)... */
            gpencil_duplicate_points(gps, &new_strokes, gpl->info);
          }
  
          /* deselect original stroke, or else the originals get moved too
           * (when using the copy + move macro)
           */
          bGPDspoint *pt;
          int i;
          for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
            pt->flag &= ~GP_SPOINT_SELECT;
          }
          gps->flag &= ~GP_STROKE_SELECT;
        }
      }
  
      /* add all new strokes in temp buffer to the frame (preventing double-copies) */
      BLI_movelisttolist(&gpf->strokes, &new_strokes);
      BLI_assert(new_strokes.first == NULL);
    }
    CTX_DATA_END;
  
    /* updates */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_duplicate(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Duplicate Strokes";
    ot->idname = "GPENCIL_OT_duplicate";
    ot->description = "Duplicate the selected Grease Pencil strokes";
  
    /* callbacks */
    ot->exec = gpencil_duplicate_exec;
    ot->poll = gpencil_stroke_edit_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Extrude Selected Strokes Operator
   * \{ */
  
  /* helper to copy a point to temp area */
  static void gpencil_copy_move_point(bGPDstroke *gps,
                                      bGPDspoint *temp_points,
                                      MDeformVert *temp_dverts,
                                      int from_idx,
                                      int to_idx,
                                      const bool copy)
  {
    bGPDspoint *pt = &temp_points[from_idx];
    bGPDspoint *pt_final = &gps->points[to_idx];
  
    copy_v3_v3(&pt_final->x, &pt->x);
    pt_final->pressure = pt->pressure;
    pt_final->strength = pt->strength;
    pt_final->time = pt->time;
    pt_final->flag = pt->flag;
    pt_final->uv_fac = pt->uv_fac;
    pt_final->uv_rot = pt->uv_rot;
    copy_v4_v4(pt_final->vert_color, pt->vert_color);
  
    if (gps->dvert != NULL) {
      MDeformVert *dvert = &temp_dverts[from_idx];
      MDeformVert *dvert_final = &gps->dvert[to_idx];
  
      dvert_final->totweight = dvert->totweight;
      /* if copy, duplicate memory, otherwise move only the pointer */
      if (copy) {
        dvert_final->dw = MEM_dupallocN(dvert->dw);
      }
      else {
        dvert_final->dw = dvert->dw;
      }
    }
  }
  
  static void gpencil_add_move_points(bGPDframe *gpf, bGPDstroke *gps)
  {
    bGPDspoint *temp_points = NULL;
    MDeformVert *temp_dverts = NULL;
    bGPDspoint *pt = NULL;
    const bGPDspoint *pt_start = &gps->points[0];
    const bGPDspoint *pt_last = &gps->points[gps->totpoints - 1];
    const bool do_first = (pt_start->flag & GP_SPOINT_SELECT);
    const bool do_last = ((pt_last->flag & GP_SPOINT_SELECT) && (pt_start != pt_last));
    const bool do_stroke = (do_first || do_last);
  
    /* review points in the middle of stroke to create new strokes */
    for (int i = 0; i < gps->totpoints; i++) {
      /* skip first and last point */
      if ((i == 0) || (i == gps->totpoints - 1)) {
        continue;
      }
  
      pt = &gps->points[i];
      if (pt->flag == GP_SPOINT_SELECT) {
        /* duplicate original stroke data */
        bGPDstroke *gps_new = BKE_gpencil_stroke_duplicate(gps, false);
        gps_new->prev = gps_new->next = NULL;
  
        /* add new points array */
        gps_new->totpoints = 1;
        gps_new->points = MEM_callocN(sizeof(bGPDspoint), __func__);
        gps_new->dvert = NULL;
  
        if (gps->dvert != NULL) {
          gps_new->dvert = MEM_callocN(sizeof(MDeformVert), __func__);
        }
  
        BLI_insertlinkafter(&gpf->strokes, gps, gps_new);
  
        /* copy selected point data to new stroke */
        gpencil_copy_move_point(gps_new, gps->points, gps->dvert, i, 0, true);
  
        /* Calc geometry data. */
        BKE_gpencil_stroke_geometry_update(gps);
        BKE_gpencil_stroke_geometry_update(gps_new);
  
        /* deselect orinal point */
        pt->flag &= ~GP_SPOINT_SELECT;
      }
    }
  
    /* review first and last point to reuse same stroke */
    int i2 = 0;
    int totnewpoints, oldtotpoints;
    /* if first or last, reuse stroke and resize */
    if ((do_first) || (do_last)) {
      totnewpoints = gps->totpoints;
      if (do_first) {
        totnewpoints++;
      }
      if (do_last) {
        totnewpoints++;
      }
  
      /* duplicate points in a temp area */
      temp_points = MEM_dupallocN(gps->points);
      oldtotpoints = gps->totpoints;
      if (gps->dvert != NULL) {
        temp_dverts = MEM_dupallocN(gps->dvert);
      }
  
      /* if first point, need move all one position */
      if (do_first) {
        i2 = 1;
      }
  
      /* resize the points arrays */
      gps->totpoints = totnewpoints;
      gps->points = MEM_recallocN(gps->points, sizeof(*gps->points) * gps->totpoints);
      if (gps->dvert != NULL) {
        gps->dvert = MEM_recallocN(gps->dvert, sizeof(*gps->dvert) * gps->totpoints);
      }
  
      /* move points to new position */
      for (int i = 0; i < oldtotpoints; i++) {
        gpencil_copy_move_point(gps, temp_points, temp_dverts, i, i2, false);
        i2++;
      }
  
      /* If first point, add new point at the beginning. */
      if (do_first) {
        gpencil_copy_move_point(gps, temp_points, temp_dverts, 0, 0, true);
        /* deselect old */
        pt = &gps->points[1];
        pt->flag &= ~GP_SPOINT_SELECT;
        /* select new */
        pt = &gps->points[0];
        pt->flag |= GP_SPOINT_SELECT;
      }
  
      /* if last point, add new point at the end */
      if (do_last) {
        gpencil_copy_move_point(
            gps, temp_points, temp_dverts, oldtotpoints - 1, gps->totpoints - 1, true);
  
        /* deselect old */
        pt = &gps->points[gps->totpoints - 2];
        pt->flag &= ~GP_SPOINT_SELECT;
        /* select new */
        pt = &gps->points[gps->totpoints - 1];
        pt->flag |= GP_SPOINT_SELECT;
      }
  
      /* Flip stroke if it was only one point to consider extrude point as last point. */
      if (gps->totpoints == 2) {
        gpencil_flip_stroke(gps);
      }
  
      /* Calc geometry data. */
      BKE_gpencil_stroke_geometry_update(gps);
  
      MEM_SAFE_FREE(temp_points);
      MEM_SAFE_FREE(temp_dverts);
    }
  
    /* if the stroke is not reused, deselect */
    if (!do_stroke) {
      gps->flag &= ~GP_STROKE_SELECT;
    }
  }
  
  static int gpencil_extrude_exec(bContext *C, wmOperator *op)
  {
    Object *obact = CTX_data_active_object(C);
    bGPdata *gpd = (bGPdata *)obact->data;
    const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd);
    bGPDstroke *gps = NULL;
  
    if (gpd == NULL) {
      BKE_report(op->reports, RPT_ERROR, "No Grease Pencil data");
      return OPERATOR_CANCELLED;
    }
  
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      bGPDframe *init_gpf = (is_multiedit) ? gpl->frames.first : gpl->actframe;
  
      for (bGPDframe *gpf = init_gpf; gpf; gpf = gpf->next) {
        if ((gpf == gpl->actframe) || ((gpf->flag & GP_FRAME_SELECT) && (is_multiedit))) {
          if (gpf == NULL) {
            continue;
          }
  
          for (gps = gpf->strokes.first; gps; gps = gps->next) {
            /* skip strokes that are invalid for current view */
            if (ED_gpencil_stroke_can_use(C, gps) == false) {
              continue;
            }
  
            if (gps->flag & GP_STROKE_SELECT) {
              gpencil_add_move_points(gpf, gps);
            }
          }
          /* if not multiedit, exit loop*/
          if (!is_multiedit) {
            break;
          }
        }
      }
    }
    CTX_DATA_END;
  
    /* updates */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_COPY_ON_WRITE);
    DEG_id_tag_update(&obact->id, ID_RECALC_COPY_ON_WRITE);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_extrude(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Extrude Stroke Points";
    ot->idname = "GPENCIL_OT_extrude";
    ot->description = "Extrude the selected Grease Pencil points";
  
    /* callbacks */
    ot->exec = gpencil_extrude_exec;
    ot->poll = gpencil_stroke_edit_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Copy/Paste Strokes Utilities
   *
   * Grease Pencil stroke data copy/paste buffer:
   * - The copy operation collects all segments of selected strokes,
   *   dumping "ready to be copied" copies of the strokes into the buffer.
   * - The paste operation makes a copy of those elements, and adds them
   *   to the active layer. This effectively flattens down the strokes
   *   from several different layers into a single layer.
   * \{ */
  
  /* list of bGPDstroke instances */
  /* NOTE: is exposed within the editors/gpencil module so that other tools can use it too */
  ListBase gpencil_strokes_copypastebuf = {NULL, NULL};
  
  /* Hash for hanging on to all the colors used by strokes in the buffer
   *
   * This is needed to prevent dangling and unsafe pointers when pasting across data-blocks,
   * or after a color used by a stroke in the buffer gets deleted (via user action or undo).
   */
  static GHash *gpencil_strokes_copypastebuf_colors = NULL;
  
  static GHash *gpencil_strokes_copypastebuf_colors_material_to_name_create(Main *bmain)
  {
    GHash *ma_to_name = BLI_ghash_ptr_new(__func__);
  
    for (Material *ma = bmain->materials.first; ma != NULL; ma = ma->id.next) {
      char *name = BKE_id_to_unique_string_key(&ma->id);
      BLI_ghash_insert(ma_to_name, ma, name);
    }
  
    return ma_to_name;
  }
  
  static void gpencil_strokes_copypastebuf_colors_material_to_name_free(GHash *ma_to_name)
  {
    BLI_ghash_free(ma_to_name, NULL, MEM_freeN);
  }
  
  static GHash *gpencil_strokes_copypastebuf_colors_name_to_material_create(Main *bmain)
  {
    GHash *name_to_ma = BLI_ghash_str_new(__func__);
  
    for (Material *ma = bmain->materials.first; ma != NULL; ma = ma->id.next) {
      char *name = BKE_id_to_unique_string_key(&ma->id);
      BLI_ghash_insert(name_to_ma, name, ma);
    }
  
    return name_to_ma;
  }
  
  static void gpencil_strokes_copypastebuf_colors_name_to_material_free(GHash *name_to_ma)
  {
    BLI_ghash_free(name_to_ma, MEM_freeN, NULL);
  }
  
  /* Free copy/paste buffer data */
  void ED_gpencil_strokes_copybuf_free(void)
  {
    bGPDstroke *gps, *gpsn;
  
    /* Free the colors buffer
     * NOTE: This is done before the strokes so that the ptrs are still safe
     */
    if (gpencil_strokes_copypastebuf_colors) {
      BLI_ghash_free(gpencil_strokes_copypastebuf_colors, NULL, MEM_freeN);
      gpencil_strokes_copypastebuf_colors = NULL;
    }
  
    /* Free the stroke buffer */
    for (gps = gpencil_strokes_copypastebuf.first; gps; gps = gpsn) {
      gpsn = gps->next;
  
      if (gps->points) {
        MEM_freeN(gps->points);
      }
      if (gps->dvert) {
        BKE_gpencil_free_stroke_weights(gps);
        MEM_freeN(gps->dvert);
      }
  
      MEM_SAFE_FREE(gps->triangles);
  
      BLI_freelinkN(&gpencil_strokes_copypastebuf, gps);
    }
  
    gpencil_strokes_copypastebuf.first = gpencil_strokes_copypastebuf.last = NULL;
  }
  
  /**
   * Ensure that destination datablock has all the colors the pasted strokes need.
   * Helper function for copy-pasting strokes
   */
  GHash *gpencil_copybuf_validate_colormap(bContext *C)
  {
    Main *bmain = CTX_data_main(C);
    Object *ob = CTX_data_active_object(C);
    GHash *new_colors = BLI_ghash_int_new("GPencil Paste Dst Colors");
    GHashIterator gh_iter;
  
    /* For each color, check if exist and add if not */
    GHash *name_to_ma = gpencil_strokes_copypastebuf_colors_name_to_material_create(bmain);
  
    GHASH_ITER (gh_iter, gpencil_strokes_copypastebuf_colors) {
      int *key = BLI_ghashIterator_getKey(&gh_iter);
      char *ma_name = BLI_ghashIterator_getValue(&gh_iter);
      Material *ma = BLI_ghash_lookup(name_to_ma, ma_name);
  
      BKE_gpencil_object_material_ensure(bmain, ob, ma);
  
      /* Store this mapping (for use later when pasting) */
      if (!BLI_ghash_haskey(new_colors, POINTER_FROM_INT(*key))) {
        BLI_ghash_insert(new_colors, POINTER_FROM_INT(*key), ma);
      }
    }
  
    gpencil_strokes_copypastebuf_colors_name_to_material_free(name_to_ma);
  
    return new_colors;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Copy Selected Strokes Operator
   * \{ */
  
  static int gpencil_strokes_copy_exec(bContext *C, wmOperator *op)
  {
    Main *bmain = CTX_data_main(C);
    Object *ob = CTX_data_active_object(C);
    bGPdata *gpd = ED_gpencil_data_get_active(C);
  
    if (gpd == NULL) {
      BKE_report(op->reports, RPT_ERROR, "No Grease Pencil data");
      return OPERATOR_CANCELLED;
    }
  
    if (GPENCIL_MULTIEDIT_SESSIONS_ON(gpd)) {
      BKE_report(op->reports, RPT_ERROR, "Operator not supported in multiframe edition");
      return OPERATOR_CANCELLED;
    }
  
    /* clear the buffer first */
    ED_gpencil_strokes_copybuf_free();
  
    /* for each visible (and editable) layer's selected strokes,
     * copy the strokes into a temporary buffer, then append
     * once all done
     */
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      bGPDframe *gpf = gpl->actframe;
      bGPDstroke *gps;
  
      if (gpf == NULL) {
        continue;
      }
  
      /* make copies of selected strokes, and deselect these once we're done */
      for (gps = gpf->strokes.first; gps; gps = gps->next) {
        /* skip strokes that are invalid for current view */
        if (ED_gpencil_stroke_can_use(C, gps) == false) {
          continue;
        }
  
        if (gps->flag & GP_STROKE_SELECT) {
          if (gps->totpoints == 1) {
            /* Special Case: If there's just a single point in this stroke... */
            bGPDstroke *gpsd;
  
            /* make direct copies of the stroke and its points */
            gpsd = BKE_gpencil_stroke_duplicate(gps, false);
  
            /* saves original layer name */
            BLI_strncpy(gpsd->runtime.tmp_layerinfo, gpl->info, sizeof(gpsd->runtime.tmp_layerinfo));
            gpsd->points = MEM_dupallocN(gps->points);
            if (gps->dvert != NULL) {
              gpsd->dvert = MEM_dupallocN(gps->dvert);
              BKE_gpencil_stroke_weights_duplicate(gps, gpsd);
            }
  
            /* Calc geometry data. */
            BKE_gpencil_stroke_geometry_update(gpsd);
  
            /* add to temp buffer */
            gpsd->next = gpsd->prev = NULL;
            BLI_addtail(&gpencil_strokes_copypastebuf, gpsd);
          }
          else {
            /* delegate to a helper, as there's too much to fit in here (for copying subsets)... */
            gpencil_duplicate_points(gps, &gpencil_strokes_copypastebuf, gpl->info);
          }
        }
      }
    }
    CTX_DATA_END;
  
    /* Build up hash of material colors used in these strokes */
    if (gpencil_strokes_copypastebuf.first) {
      gpencil_strokes_copypastebuf_colors = BLI_ghash_int_new("GPencil CopyBuf Colors");
      GHash *ma_to_name = gpencil_strokes_copypastebuf_colors_material_to_name_create(bmain);
      LISTBASE_FOREACH (bGPDstroke *, gps, &gpencil_strokes_copypastebuf) {
        if (ED_gpencil_stroke_can_use(C, gps)) {
          Material *ma = BKE_object_material_get(ob, gps->mat_nr + 1);
          /* Avoid default material. */
          if (ma == NULL) {
            continue;
          }
  
          char **ma_name_val;
          if (!BLI_ghash_ensure_p(
                  gpencil_strokes_copypastebuf_colors, &gps->mat_nr, (void ***)&ma_name_val)) {
            char *ma_name = BLI_ghash_lookup(ma_to_name, ma);
            *ma_name_val = MEM_dupallocN(ma_name);
          }
        }
      }
      gpencil_strokes_copypastebuf_colors_material_to_name_free(ma_to_name);
    }
  
    /* updates (to ensure operator buttons are refreshed, when used via hotkeys) */
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA, NULL);  // XXX?
  
    /* done */
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_copy(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Copy Strokes";
    ot->idname = "GPENCIL_OT_copy";
    ot->description = "Copy selected Grease Pencil points and strokes";
  
    /* callbacks */
    ot->exec = gpencil_strokes_copy_exec;
    ot->poll = gpencil_stroke_edit_poll;
  
    /* flags */
    // ot->flag = OPTYPE_REGISTER;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Paste Selected Strokes Operator
   * \{ */
  
  static bool gpencil_strokes_paste_poll(bContext *C)
  {
    ScrArea *area = CTX_wm_area(C);
    if (!((area != NULL) && (area->spacetype == SPACE_VIEW3D))) {
      return false;
    }
    /* 1) Must have GP datablock to paste to
     *    - We don't need to have an active layer though, as that can easily get added
     *    - If the active layer is locked, we can't paste there,
     *      but that should prompt a warning instead.
     * 2) Copy buffer must at least have something (though it may be the wrong sort...).
     */
    return (ED_gpencil_data_get_active(C) != NULL) &&
           (!BLI_listbase_is_empty(&gpencil_strokes_copypastebuf));
  }
  
  typedef enum eGP_PasteMode {
    GP_COPY_BY_LAYER = -1,
    GP_COPY_TO_ACTIVE = 1,
  } eGP_PasteMode;
  
  static int gpencil_strokes_paste_exec(bContext *C, wmOperator *op)
  {
    Object *ob = CTX_data_active_object(C);
    bGPdata *gpd = (bGPdata *)ob->data;
    bGPDlayer *gpl = BKE_gpencil_layer_active_get(gpd); /* only use active for copy merge */
    Scene *scene = CTX_data_scene(C);
    bGPDframe *gpf;
  
    eGP_PasteMode type = RNA_enum_get(op->ptr, "type");
    const bool on_back = RNA_boolean_get(op->ptr, "paste_back");
    GHash *new_colors;
  
    /* Check for various error conditions. */
    if (GPENCIL_MULTIEDIT_SESSIONS_ON(gpd)) {
      BKE_report(op->reports, RPT_ERROR, "Operator not supported in multiframe edition");
      return OPERATOR_CANCELLED;
    }
  
    if (BLI_listbase_is_empty(&gpencil_strokes_copypastebuf)) {
      BKE_report(op->reports,
                 RPT_ERROR,
                 "No strokes to paste, select and copy some points before trying again");
      return OPERATOR_CANCELLED;
    }
  
    if (gpl == NULL) {
      /* no active layer - let's just create one */
      gpl = BKE_gpencil_layer_addnew(gpd, DATA_("GP_Layer"), true);
    }
    else if ((BKE_gpencil_layer_is_editable(gpl) == false) && (type == GP_COPY_TO_ACTIVE)) {
      BKE_report(
          op->reports, RPT_ERROR, "Can not paste strokes when active layer is hidden or locked");
      return OPERATOR_CANCELLED;
    }
    else {
      /* Check that some of the strokes in the buffer can be used */
      bGPDstroke *gps;
      bool ok = false;
  
      for (gps = gpencil_strokes_copypastebuf.first; gps; gps = gps->next) {
        if (ED_gpencil_stroke_can_use(C, gps)) {
          ok = true;
          break;
        }
      }
  
      if (ok == false) {
        return OPERATOR_CANCELLED;
      }
    }
  
    /* Deselect all strokes first */
    CTX_DATA_BEGIN (C, bGPDstroke *, gps, editable_gpencil_strokes) {
      bGPDspoint *pt;
      int i;
  
      for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
        pt->flag &= ~GP_SPOINT_SELECT;
      }
  
      gps->flag &= ~GP_STROKE_SELECT;
    }
    CTX_DATA_END;
  
    /* Ensure that all the necessary colors exist */
    new_colors = gpencil_copybuf_validate_colormap(C);
  
    /* Copy over the strokes from the buffer (and adjust the colors) */
    bGPDstroke *gps_init = (!on_back) ? gpencil_strokes_copypastebuf.first :
                                        gpencil_strokes_copypastebuf.last;
    for (bGPDstroke *gps = gps_init; gps; gps = (!on_back) ? gps->next : gps->prev) {
      if (ED_gpencil_stroke_can_use(C, gps)) {
        /* Need to verify if layer exists */
        if (type != GP_COPY_TO_ACTIVE) {
          gpl = BLI_findstring(&gpd->layers, gps->runtime.tmp_layerinfo, offsetof(bGPDlayer, info));
          if (gpl == NULL) {
            /* no layer - use active (only if layer deleted before paste) */
            gpl = BKE_gpencil_layer_active_get(gpd);
          }
        }
  
        /* Ensure we have a frame to draw into
         * NOTE: Since this is an op which creates strokes,
         *       we are obliged to add a new frame if one
         *       doesn't exist already
         */
        gpf = BKE_gpencil_layer_frame_get(gpl, CFRA, GP_GETFRAME_ADD_NEW);
        if (gpf) {
          /* Create new stroke */
          bGPDstroke *new_stroke = BKE_gpencil_stroke_duplicate(gps, true);
          new_stroke->runtime.tmp_layerinfo[0] = '\0';
          new_stroke->next = new_stroke->prev = NULL;
  
          /* Calc geometry data. */
          BKE_gpencil_stroke_geometry_update(new_stroke);
  
          if (on_back) {
            BLI_addhead(&gpf->strokes, new_stroke);
          }
          else {
            BLI_addtail(&gpf->strokes, new_stroke);
          }
  
          /* Remap material */
          Material *ma = BLI_ghash_lookup(new_colors, POINTER_FROM_INT(new_stroke->mat_nr));
          new_stroke->mat_nr = BKE_gpencil_object_material_index_get(ob, ma);
          CLAMP_MIN(new_stroke->mat_nr, 0);
        }
      }
    }
  
    /* free temp data */
    BLI_ghash_free(new_colors, NULL, NULL);
  
    /* updates */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_paste(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    static const EnumPropertyItem copy_type[] = {
        {GP_COPY_TO_ACTIVE, "ACTIVE", 0, "Paste to Active", ""},
        {GP_COPY_BY_LAYER, "LAYER", 0, "Paste by Layer", ""},
        {0, NULL, 0, NULL, NULL},
    };
  
    /* identifiers */
    ot->name = "Paste Strokes";
    ot->idname = "GPENCIL_OT_paste";
    ot->description = "Paste previously copied strokes to active layer or to original layer";
  
    /* callbacks */
    ot->exec = gpencil_strokes_paste_exec;
    ot->poll = gpencil_strokes_paste_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    ot->prop = RNA_def_enum(ot->srna, "type", copy_type, GP_COPY_TO_ACTIVE, "Type", "");
  
    prop = RNA_def_boolean(
        ot->srna, "paste_back", 0, "Paste on Back", "Add pasted strokes behind all strokes");
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Move To Layer Operator
   * \{ */
  
  static int gpencil_move_to_layer_exec(bContext *C, wmOperator *op)
  {
    Object *ob = CTX_data_active_object(C);
    bGPdata *gpd = (bGPdata *)ob->data;
    Scene *scene = CTX_data_scene(C);
    bGPDlayer *target_layer = NULL;
    ListBase strokes = {NULL, NULL};
    int layer_num = RNA_int_get(op->ptr, "layer");
    const bool use_autolock = (bool)(gpd->flag & GP_DATA_AUTOLOCK_LAYERS);
  
    if (GPENCIL_MULTIEDIT_SESSIONS_ON(gpd)) {
      BKE_report(op->reports, RPT_ERROR, "Operator not supported in multiframe edition");
      return OPERATOR_CANCELLED;
    }
  
    /* if autolock enabled, disabled now */
    if (use_autolock) {
      gpd->flag &= ~GP_DATA_AUTOLOCK_LAYERS;
    }
  
    /* Try to get layer */
    if (layer_num > -1) {
      target_layer = BLI_findlink(&gpd->layers, layer_num);
    }
    else {
      /* Create a new layer. */
      target_layer = BKE_gpencil_layer_addnew(gpd, "GP_Layer", true);
    }
  
    if (target_layer == NULL) {
      /* back autolock status */
      if (use_autolock) {
        gpd->flag |= GP_DATA_AUTOLOCK_LAYERS;
      }
      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;
  
      /* 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 */
      LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) {
  
        /* skip strokes that are invalid for current view */
        if (ED_gpencil_stroke_can_use(C, gps) == false) {
          continue;
        }
  
        /* Check if the color is editable. */
        if (ED_gpencil_stroke_color_use(ob, gpl, gps) == false) {
          continue;
        }
  
        /* 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);
        }
      }
  
      /* if new layer and autolock, lock old layer */
      if ((layer_num == -1) && (use_autolock)) {
        gpl->flag |= GP_LAYER_LOCKED;
      }
    }
    CTX_DATA_END;
  
    /* Paste them all in one go */
    if (strokes.first) {
      bGPDframe *gpf = BKE_gpencil_layer_frame_get(target_layer, CFRA, GP_GETFRAME_ADD_NEW);
  
      BLI_movelisttolist(&gpf->strokes, &strokes);
      BLI_assert((strokes.first == strokes.last) && (strokes.first == NULL));
    }
  
    /* back autolock status */
    if (use_autolock) {
      gpd->flag |= GP_DATA_AUTOLOCK_LAYERS;
    }
  
    /* updates */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_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->exec = gpencil_move_to_layer_exec;
    ot->poll = gpencil_stroke_edit_poll;  // XXX?
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* GPencil layer to use. */
    ot->prop = RNA_def_int(
        ot->srna, "layer", 0, -1, INT_MAX, "Grease Pencil Layer", "", -1, INT_MAX);
    RNA_def_property_flag(ot->prop, PROP_HIDDEN | PROP_SKIP_SAVE);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Add Blank Frame Operator
   * \{ */
  
  static int gpencil_blank_frame_add_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    Scene *scene = CTX_data_scene(C);
    int cfra = CFRA;
  
    bGPDlayer *active_gpl = BKE_gpencil_layer_active_get(gpd);
  
    const bool all_layers = RNA_boolean_get(op->ptr, "all_layers");
  
    /* Initialize data-block and an active layer if nothing exists yet. */
    if (ELEM(NULL, gpd, active_gpl)) {
      /* Let's just be lazy, and call the "Add New Layer" operator,
       * which sets everything up as required. */
      WM_operator_name_call(C, "GPENCIL_OT_layer_add", WM_OP_EXEC_DEFAULT, NULL);
    }
  
    /* Go through each layer, adding a frame after the active one
     * and/or shunting all the others out of the way
     */
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      if ((all_layers == false) && (gpl != active_gpl)) {
        continue;
      }
  
      /* 1) Check for an existing frame on the current frame */
      bGPDframe *gpf = BKE_gpencil_layer_frame_find(gpl, cfra);
      if (gpf) {
        /* Shunt all frames after (and including) the existing one later by 1-frame */
        for (; gpf; gpf = gpf->next) {
          gpf->framenum += 1;
        }
      }
  
      /* 2) Now add a new frame, with nothing in it */
      gpl->actframe = BKE_gpencil_layer_frame_get(gpl, cfra, GP_GETFRAME_ADD_NEW);
    }
    CTX_DATA_END;
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_blank_frame_add(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Insert Blank Frame";
    ot->idname = "GPENCIL_OT_blank_frame_add";
    ot->description =
        "Insert a blank frame on the current frame "
        "(all subsequently existing frames, if any, are shifted right by one frame)";
  
    /* callbacks */
    ot->exec = gpencil_blank_frame_add_exec;
    ot->poll = gpencil_add_poll;
  
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    prop = RNA_def_boolean(ot->srna,
                           "all_layers",
                           false,
                           "All Layers",
                           "Create blank frame in all layers, not only active");
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Delete Active Frame Operator
   * \{ */
  
  static bool gpencil_actframe_delete_poll(bContext *C)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    bGPDlayer *gpl = BKE_gpencil_layer_active_get(gpd);
  
    /* only if there's an active layer with an active frame */
    return (gpl && gpl->actframe);
  }
  
  static bool annotation_actframe_delete_poll(bContext *C)
  {
    bGPdata *gpd = ED_annotation_data_get_active(C);
    bGPDlayer *gpl = BKE_gpencil_layer_active_get(gpd);
  
    /* only if there's an active layer with an active frame */
    return (gpl && gpl->actframe);
  }
  
  /* delete active frame - wrapper around API calls */
  static int gpencil_actframe_delete_exec(bContext *C, wmOperator *op)
  {
    const bool is_annotation = STREQ(op->idname, "GPENCIL_OT_annotation_active_frame_delete");
  
    bGPdata *gpd = (!is_annotation) ? ED_gpencil_data_get_active(C) :
                                      ED_annotation_data_get_active(C);
  
    bGPDlayer *gpl = BKE_gpencil_layer_active_get(gpd);
  
    Scene *scene = CTX_data_scene(C);
  
    bGPDframe *gpf = BKE_gpencil_layer_frame_get(gpl, CFRA, GP_GETFRAME_USE_PREV);
  
    /* if there's no existing Grease-Pencil data there, add some */
    if (gpd == NULL) {
      BKE_report(op->reports, RPT_ERROR, "No grease pencil data");
      return OPERATOR_CANCELLED;
    }
    if (ELEM(NULL, gpl, gpf)) {
      BKE_report(op->reports, RPT_ERROR, "No active frame to delete");
      return OPERATOR_CANCELLED;
    }
  
    /* delete it... */
    BKE_gpencil_layer_frame_delete(gpl, gpf);
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_active_frame_delete(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Delete Active Frame";
    ot->idname = "GPENCIL_OT_active_frame_delete";
    ot->description = "Delete the active frame for the active Grease Pencil Layer";
  
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* callbacks */
    ot->exec = gpencil_actframe_delete_exec;
    ot->poll = gpencil_actframe_delete_poll;
  }
  
  void GPENCIL_OT_annotation_active_frame_delete(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Delete Active Frame";
    ot->idname = "GPENCIL_OT_annotation_active_frame_delete";
    ot->description = "Delete the active frame for the active Annotation Layer";
  
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* callbacks */
    ot->exec = gpencil_actframe_delete_exec;
    ot->poll = annotation_actframe_delete_poll;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Delete All Active Frames
   * \{ */
  
  static bool gpencil_actframe_delete_all_poll(bContext *C)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
  
    /* 1) There must be grease pencil data
     * 2) Hopefully some of the layers have stuff we can use
     */
    return (gpd && gpd->layers.first);
  }
  
  static int gpencil_actframe_delete_all_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    Scene *scene = CTX_data_scene(C);
  
    bool success = false;
  
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      /* try to get the "active" frame - but only if it actually occurs on this frame */
      bGPDframe *gpf = BKE_gpencil_layer_frame_get(gpl, CFRA, GP_GETFRAME_USE_PREV);
  
      if (gpf == NULL) {
        continue;
      }
  
      /* delete it... */
      BKE_gpencil_layer_frame_delete(gpl, gpf);
  
      /* we successfully modified something */
      success = true;
    }
    CTX_DATA_END;
  
    /* updates */
    if (success) {
      DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
      WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
      return OPERATOR_FINISHED;
    }
    BKE_report(op->reports, RPT_ERROR, "No active frame(s) to delete");
    return OPERATOR_CANCELLED;
  }
  
  void GPENCIL_OT_active_frames_delete_all(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Delete All Active Frames";
    ot->idname = "GPENCIL_OT_active_frames_delete_all";
    ot->description = "Delete the active frame(s) of all editable Grease Pencil layers";
  
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* callbacks */
    ot->exec = gpencil_actframe_delete_all_exec;
    ot->poll = gpencil_actframe_delete_all_poll;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Delete/Dissolve Utilities
   * \{ */
  
  typedef enum eGP_DeleteMode {
    /* delete selected stroke points */
    GP_DELETEOP_POINTS = 0,
    /* delete selected strokes */
    GP_DELETEOP_STROKES = 1,
    /* delete active frame */
    GP_DELETEOP_FRAME = 2,
  } eGP_DeleteMode;
  
  typedef enum eGP_DissolveMode {
    /* dissolve all selected points */
    GP_DISSOLVE_POINTS = 0,
    /* dissolve between selected points */
    GP_DISSOLVE_BETWEEN = 1,
    /* dissolve unselected points */
    GP_DISSOLVE_UNSELECT = 2,
  } eGP_DissolveMode;
  
  /* Delete selected strokes */
  static int gpencil_delete_selected_strokes(bContext *C)
  {
    bool changed = false;
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd);
  
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      bGPDframe *init_gpf = (is_multiedit) ? gpl->frames.first : gpl->actframe;
  
      for (bGPDframe *gpf = init_gpf; gpf; gpf = gpf->next) {
        if ((gpf == gpl->actframe) || ((gpf->flag & GP_FRAME_SELECT) && (is_multiedit))) {
  
          if (gpf == NULL) {
            continue;
          }
  
          /* simply delete strokes which are selected */
          LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) {
  
            /* skip strokes that are invalid for current view */
            if (ED_gpencil_stroke_can_use(C, gps) == false) {
              continue;
            }
  
            /* free stroke if selected */
            if (gps->flag & GP_STROKE_SELECT) {
              /* free stroke memory arrays, then stroke itself */
              if (gps->points) {
                MEM_freeN(gps->points);
              }
              if (gps->dvert) {
                BKE_gpencil_free_stroke_weights(gps);
                MEM_freeN(gps->dvert);
              }
              MEM_SAFE_FREE(gps->triangles);
              BLI_freelinkN(&gpf->strokes, gps);
  
              changed = true;
            }
          }
        }
      }
    }
    CTX_DATA_END;
  
    if (changed) {
      DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
      WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
      return OPERATOR_FINISHED;
    }
    return OPERATOR_CANCELLED;
  }
  
  /* ----------------------------------- */
  
  /* Delete selected points but keep the stroke */
  static int gpencil_dissolve_selected_points(bContext *C, eGP_DissolveMode mode)
  {
    Object *ob = CTX_data_active_object(C);
    bGPdata *gpd = (bGPdata *)ob->data;
    bool changed = false;
    int first = 0;
    int last = 0;
  
    GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) {
      /* the stroke must have at least one point selected for any operator */
      if (gps->flag & GP_STROKE_SELECT) {
        bGPDspoint *pt;
        MDeformVert *dvert = NULL;
        int i;
  
        int tot = gps->totpoints; /* number of points in new buffer */
  
        /* first pass: count points to remove */
        switch (mode) {
          case GP_DISSOLVE_POINTS:
            /* Count how many points are selected (i.e. how many to remove) */
            for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
              if (pt->flag & GP_SPOINT_SELECT) {
                /* selected point - one of the points to remove */
                tot--;
              }
            }
            break;
          case GP_DISSOLVE_BETWEEN:
            /* need to find first and last point selected */
            first = -1;
            last = 0;
            for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
              if (pt->flag & GP_SPOINT_SELECT) {
                if (first < 0) {
                  first = i;
                }
                last = i;
              }
            }
            /* count unselected points in the range */
            for (i = first, pt = gps->points + first; i < last; i++, pt++) {
              if ((pt->flag & GP_SPOINT_SELECT) == 0) {
                tot--;
              }
            }
            break;
          case GP_DISSOLVE_UNSELECT:
            /* count number of unselected points */
            for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
              if ((pt->flag & GP_SPOINT_SELECT) == 0) {
                tot--;
              }
            }
            break;
          default:
            return false;
            break;
        }
  
        /* if no points are left, we simply delete the entire stroke */
        if (tot <= 0) {
          /* remove the entire stroke */
          if (gps->points) {
            MEM_freeN(gps->points);
          }
          if (gps->dvert) {
            BKE_gpencil_free_stroke_weights(gps);
            MEM_freeN(gps->dvert);
          }
          if (gps->triangles) {
            MEM_freeN(gps->triangles);
          }
          BLI_freelinkN(&gpf_->strokes, gps);
          DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
        }
        else {
          /* just copy all points to keep into a smaller buffer */
          bGPDspoint *new_points = MEM_callocN(sizeof(bGPDspoint) * tot,
                                               "new gp stroke points copy");
          bGPDspoint *npt = new_points;
  
          MDeformVert *new_dvert = NULL;
          MDeformVert *ndvert = NULL;
  
          if (gps->dvert != NULL) {
            new_dvert = MEM_callocN(sizeof(MDeformVert) * tot, "new gp stroke weights copy");
            ndvert = new_dvert;
          }
  
          switch (mode) {
            case GP_DISSOLVE_POINTS:
              (gps->dvert != NULL) ? dvert = gps->dvert : NULL;
              for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
                if ((pt->flag & GP_SPOINT_SELECT) == 0) {
                  *npt = *pt;
                  npt++;
  
                  if (gps->dvert != NULL) {
                    *ndvert = *dvert;
                    ndvert->dw = MEM_dupallocN(dvert->dw);
                    ndvert++;
                  }
                }
                if (gps->dvert != NULL) {
                  dvert++;
                }
              }
              break;
            case GP_DISSOLVE_BETWEEN:
              /* copy first segment */
              (gps->dvert != NULL) ? dvert = gps->dvert : NULL;
              for (i = 0, pt = gps->points; i < first; i++, pt++) {
                *npt = *pt;
                npt++;
  
                if (gps->dvert != NULL) {
                  *ndvert = *dvert;
                  ndvert->dw = MEM_dupallocN(dvert->dw);
                  ndvert++;
                  dvert++;
                }
              }
              /* copy segment (selected points) */
              (gps->dvert != NULL) ? dvert = gps->dvert + first : NULL;
              for (i = first, pt = gps->points + first; i < last; i++, pt++) {
                if (pt->flag & GP_SPOINT_SELECT) {
                  *npt = *pt;
                  npt++;
  
                  if (gps->dvert != NULL) {
                    *ndvert = *dvert;
                    ndvert->dw = MEM_dupallocN(dvert->dw);
                    ndvert++;
                  }
                }
                if (gps->dvert != NULL) {
                  dvert++;
                }
              }
              /* copy last segment */
              (gps->dvert != NULL) ? dvert = gps->dvert + last : NULL;
              for (i = last, pt = gps->points + last; i < gps->totpoints; i++, pt++) {
                *npt = *pt;
                npt++;
  
                if (gps->dvert != NULL) {
                  *ndvert = *dvert;
                  ndvert->dw = MEM_dupallocN(dvert->dw);
                  ndvert++;
                  dvert++;
                }
              }
  
              break;
            case GP_DISSOLVE_UNSELECT:
              /* copy any selected point */
              (gps->dvert != NULL) ? dvert = gps->dvert : NULL;
              for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
                if (pt->flag & GP_SPOINT_SELECT) {
                  *npt = *pt;
                  npt++;
  
                  if (gps->dvert != NULL) {
                    *ndvert = *dvert;
                    ndvert->dw = MEM_dupallocN(dvert->dw);
                    ndvert++;
                  }
                }
                if (gps->dvert != NULL) {
                  dvert++;
                }
              }
              break;
          }
  
          /* free the old buffer */
          if (gps->points) {
            MEM_freeN(gps->points);
          }
          if (gps->dvert) {
            BKE_gpencil_free_stroke_weights(gps);
            MEM_freeN(gps->dvert);
          }
  
          /* save the new buffer */
          gps->points = new_points;
          gps->dvert = new_dvert;
          gps->totpoints = tot;
  
          /* Calc geometry data. */
          BKE_gpencil_stroke_geometry_update(gps);
  
          /* deselect the stroke, since none of its selected points will still be selected */
          gps->flag &= ~GP_STROKE_SELECT;
          for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
            pt->flag &= ~GP_SPOINT_SELECT;
          }
        }
  
        changed = true;
      }
    }
    GP_EDITABLE_STROKES_END(gpstroke_iter);
  
    if (changed) {
      DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
      WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
      return OPERATOR_FINISHED;
    }
    return OPERATOR_CANCELLED;
  }
  
  /* ----------------------------------- */
  
  /* 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;
  
  static void gpencil_stroke_join_islands(bGPDframe *gpf,
                                          bGPDstroke *gps_first,
                                          bGPDstroke *gps_last)
  {
    bGPDspoint *pt = NULL;
    bGPDspoint *pt_final = NULL;
    const int totpoints = gps_first->totpoints + gps_last->totpoints;
  
    /* create new stroke */
    bGPDstroke *join_stroke = BKE_gpencil_stroke_duplicate(gps_first, false);
  
    join_stroke->points = MEM_callocN(sizeof(bGPDspoint) * totpoints, __func__);
    join_stroke->totpoints = totpoints;
    join_stroke->flag &= ~GP_STROKE_CYCLIC;
  
    /* copy points (last before) */
    int e1 = 0;
    int e2 = 0;
    float delta = 0.0f;
  
    for (int i = 0; i < totpoints; i++) {
      pt_final = &join_stroke->points[i];
      if (i < gps_last->totpoints) {
        pt = &gps_last->points[e1];
        e1++;
      }
      else {
        pt = &gps_first->points[e2];
        e2++;
      }
  
      /* copy current point */
      copy_v3_v3(&pt_final->x, &pt->x);
      pt_final->pressure = pt->pressure;
      pt_final->strength = pt->strength;
      pt_final->time = delta;
      pt_final->flag = pt->flag;
      copy_v4_v4(pt_final->vert_color, pt->vert_color);
  
      /* retiming with fixed time interval (we cannot determine real time) */
      delta += 0.01f;
    }
  
    /* Copy over vertex weight data (if available) */
    if ((gps_first->dvert != NULL) || (gps_last->dvert != NULL)) {
      join_stroke->dvert = MEM_callocN(sizeof(MDeformVert) * totpoints, __func__);
      MDeformVert *dvert_src = NULL;
      MDeformVert *dvert_dst = NULL;
  
      /* Copy weights (last before)*/
      e1 = 0;
      e2 = 0;
      for (int i = 0; i < totpoints; i++) {
        dvert_dst = &join_stroke->dvert[i];
        dvert_src = NULL;
        if (i < gps_last->totpoints) {
          if (gps_last->dvert) {
            dvert_src = &gps_last->dvert[e1];
            e1++;
          }
        }
        else {
          if (gps_first->dvert) {
            dvert_src = &gps_first->dvert[e2];
            e2++;
          }
        }
  
        if ((dvert_src) && (dvert_src->dw)) {
          dvert_dst->dw = MEM_dupallocN(dvert_src->dw);
        }
      }
    }
  
    /* add new stroke at head */
    BLI_addhead(&gpf->strokes, join_stroke);
    /* Calc geometry data. */
    BKE_gpencil_stroke_geometry_update(join_stroke);
  
    /* remove first stroke */
    BLI_remlink(&gpf->strokes, gps_first);
    BKE_gpencil_free_stroke(gps_first);
  
    /* remove last stroke */
    BLI_remlink(&gpf->strokes, gps_last);
    BKE_gpencil_free_stroke(gps_last);
  }
  
  /* Split the given stroke into several new strokes, partitioning
   * it based on whether the stroke points have a particular flag
   * is set (e.g. "GP_SPOINT_SELECT" in most cases, but not always)
   *
   * The algorithm used here is as follows:
   * 1) We firstly identify the number of "islands" of non-tagged points
   *    which will all end up being in new strokes.
   *    - In the most extreme case (i.e. every other vert is a 1-vert island),
   *      we have at most n / 2 islands
   *    - Once we start having larger islands than that, the number required
   *      becomes much less
   * 2) Each island gets converted to a new stroke
   * If the number of points is <= limit, the stroke is deleted
   */
  void gpencil_stroke_delete_tagged_points(bGPDframe *gpf,
                                           bGPDstroke *gps,
                                           bGPDstroke *next_stroke,
                                           int tag_flags,
                                           bool select,
                                           int limit)
  {
    tGPDeleteIsland *islands = MEM_callocN(sizeof(tGPDeleteIsland) * (gps->totpoints + 1) / 2,
                                           "gp_point_islands");
    bool in_island = false;
    int num_islands = 0;
  
    bGPDstroke *gps_first = NULL;
    const bool is_cyclic = (bool)(gps->flag & GP_STROKE_CYCLIC);
  
    /* First Pass: Identify start/end of islands */
    bGPDspoint *pt = gps->points;
    for (int i = 0; i < gps->totpoints; i++, pt++) {
      if (pt->flag & tag_flags) {
        /* selected - stop accumulating to island */
        in_island = false;
      }
      else {
        /* unselected - start of a new island? */
        int idx;
  
        if (in_island) {
          /* extend existing island */
          idx = num_islands - 1;
          islands[idx].end_idx = i;
        }
        else {
          /* start of new island */
          in_island = true;
          num_islands++;
  
          idx = num_islands - 1;
          islands[idx].start_idx = islands[idx].end_idx = i;
        }
      }
    }
  
    /* Watch out for special case where No islands = All points selected = Delete Stroke only */
    if (num_islands) {
      /* There are islands, so create a series of new strokes,
       * adding them before the "next" stroke. */
      int idx;
      bGPDstroke *new_stroke = NULL;
  
      /* Create each new stroke... */
      for (idx = 0; idx < num_islands; idx++) {
        tGPDeleteIsland *island = &islands[idx];
        new_stroke = BKE_gpencil_stroke_duplicate(gps, false);
  
        /* if cyclic and first stroke, save to join later */
        if ((is_cyclic) && (gps_first == NULL)) {
          gps_first = new_stroke;
        }
  
        new_stroke->flag &= ~GP_STROKE_CYCLIC;
  
        /* Compute new buffer size (+ 1 needed as the endpoint index is "inclusive") */
        new_stroke->totpoints = island->end_idx - island->start_idx + 1;
  
        /* Copy over the relevant point data */
        new_stroke->points = MEM_callocN(sizeof(bGPDspoint) * new_stroke->totpoints,
                                         "gp delete stroke fragment");
        memcpy(new_stroke->points,
               gps->points + island->start_idx,
               sizeof(bGPDspoint) * new_stroke->totpoints);
  
        /* Copy over vertex weight data (if available) */
        if (gps->dvert != NULL) {
          /* Copy over the relevant vertex-weight points */
          new_stroke->dvert = MEM_callocN(sizeof(MDeformVert) * new_stroke->totpoints,
                                          "gp delete stroke fragment weight");
          memcpy(new_stroke->dvert,
                 gps->dvert + island->start_idx,
                 sizeof(MDeformVert) * new_stroke->totpoints);
  
          /* Copy weights */
          int e = island->start_idx;
          for (int i = 0; i < new_stroke->totpoints; i++) {
            MDeformVert *dvert_src = &gps->dvert[e];
            MDeformVert *dvert_dst = &new_stroke->dvert[i];
            if (dvert_src->dw) {
              dvert_dst->dw = MEM_dupallocN(dvert_src->dw);
            }
            e++;
          }
        }
        /* Each island corresponds to a new stroke.
         * We must adjust the timings of these new strokes:
         *
         * Each point's timing data is a delta from stroke's inittime, so as we erase some points
         * from the start of the stroke, we have to offset this inittime and all remaining points'
         * delta values. This way we get a new stroke with exactly the same timing as if user had
         * started drawing from the first non-removed point.
         */
        {
          bGPDspoint *pts;
          float delta = gps->points[island->start_idx].time;
          int j;
  
          new_stroke->inittime += (double)delta;
  
          pts = new_stroke->points;
          for (j = 0; j < new_stroke->totpoints; j++, pts++) {
            pts->time -= delta;
            /* set flag for select again later */
            if (select == true) {
              pts->flag &= ~GP_SPOINT_SELECT;
              pts->flag |= GP_SPOINT_TAG;
            }
          }
        }
  
        /* Add new stroke to the frame or delete if below limit */
        if ((limit > 0) && (new_stroke->totpoints <= limit)) {
          BKE_gpencil_free_stroke(new_stroke);
        }
        else {
          /* Calc geometry data. */
          BKE_gpencil_stroke_geometry_update(new_stroke);
  
          if (next_stroke) {
            BLI_insertlinkbefore(&gpf->strokes, next_stroke, new_stroke);
          }
          else {
            BLI_addtail(&gpf->strokes, new_stroke);
          }
        }
      }
      /* if cyclic, need to join last stroke with first stroke */
      if ((is_cyclic) && (gps_first != NULL) && (gps_first != new_stroke)) {
        gpencil_stroke_join_islands(gpf, gps_first, new_stroke);
      }
    }
  
    /* free islands */
    MEM_freeN(islands);
  
    /* Delete the old stroke */
    BLI_remlink(&gpf->strokes, gps);
    BKE_gpencil_free_stroke(gps);
  }
  
  /* Split selected strokes into segments, splitting on selected points */
  static int gpencil_delete_selected_points(bContext *C)
  {
    Object *ob = CTX_data_active_object(C);
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd);
    bool changed = false;
  
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      bGPDframe *init_gpf = (is_multiedit) ? gpl->frames.first : gpl->actframe;
  
      for (bGPDframe *gpf = init_gpf; gpf; gpf = gpf->next) {
        if ((gpf == gpl->actframe) || ((gpf->flag & GP_FRAME_SELECT) && (is_multiedit))) {
  
          if (gpf == NULL) {
            continue;
          }
  
          /* simply delete strokes which are selected */
          LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) {
  
            /* skip strokes that are invalid for current view */
            if (ED_gpencil_stroke_can_use(C, gps) == false) {
              continue;
            }
            /* check if the color is editable */
            if (ED_gpencil_stroke_color_use(ob, gpl, gps) == false) {
              continue;
            }
  
            if (gps->flag & GP_STROKE_SELECT) {
              /* deselect old stroke, since it will be used as template for the new strokes */
              gps->flag &= ~GP_STROKE_SELECT;
  
              /* delete unwanted points by splitting stroke into several smaller ones */
              gpencil_stroke_delete_tagged_points(gpf, gps, gps->next, GP_SPOINT_SELECT, false, 0);
  
              changed = true;
            }
          }
        }
      }
    }
    CTX_DATA_END;
  
    if (changed) {
      DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
      WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
      return OPERATOR_FINISHED;
    }
    return OPERATOR_CANCELLED;
  }
  
  /* simple wrapper to external call */
  int gpencil_delete_selected_point_wrap(bContext *C)
  {
    return gpencil_delete_selected_points(C);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Delete Operator
   * \{ */
  
  static int gpencil_delete_exec(bContext *C, wmOperator *op)
  {
    eGP_DeleteMode mode = RNA_enum_get(op->ptr, "type");
    int result = OPERATOR_CANCELLED;
  
    switch (mode) {
      case GP_DELETEOP_STROKES: /* selected strokes */
        result = gpencil_delete_selected_strokes(C);
        break;
  
      case GP_DELETEOP_POINTS: /* selected points (breaks the stroke into segments) */
        result = gpencil_delete_selected_points(C);
        break;
  
      case GP_DELETEOP_FRAME: /* active frame */
        result = gpencil_actframe_delete_exec(C, op);
        break;
    }
  
    return result;
  }
  
  void GPENCIL_OT_delete(wmOperatorType *ot)
  {
    static const EnumPropertyItem prop_gpencil_delete_types[] = {
        {GP_DELETEOP_POINTS,
         "POINTS",
         0,
         "Points",
         "Delete selected points and split strokes into segments"},
        {GP_DELETEOP_STROKES, "STROKES", 0, "Strokes", "Delete selected strokes"},
        {GP_DELETEOP_FRAME, "FRAME", 0, "Frame", "Delete active frame"},
        {0, NULL, 0, NULL, NULL},
    };
  
    /* identifiers */
    ot->name = "Delete";
    ot->idname = "GPENCIL_OT_delete";
    ot->description = "Delete selected Grease Pencil strokes, vertices, or frames";
  
    /* callbacks */
    ot->invoke = WM_menu_invoke;
    ot->exec = gpencil_delete_exec;
    ot->poll = gpencil_stroke_edit_poll;
  
    /* flags */
    ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
  
    /* props */
    ot->prop = RNA_def_enum(ot->srna,
                            "type",
                            prop_gpencil_delete_types,
                            0,
                            "Type",
                            "Method used for deleting Grease Pencil data");
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Dissolve Operator
   * \{ */
  
  static int gpencil_dissolve_exec(bContext *C, wmOperator *op)
  {
    eGP_DissolveMode mode = RNA_enum_get(op->ptr, "type");
  
    return gpencil_dissolve_selected_points(C, mode);
  }
  
  void GPENCIL_OT_dissolve(wmOperatorType *ot)
  {
    static EnumPropertyItem prop_gpencil_dissolve_types[] = {
        {GP_DISSOLVE_POINTS, "POINTS", 0, "Dissolve", "Dissolve selected points"},
        {GP_DISSOLVE_BETWEEN,
         "BETWEEN",
         0,
         "Dissolve Between",
         "Dissolve points between selected points"},
        {GP_DISSOLVE_UNSELECT, "UNSELECT", 0, "Dissolve Unselect", "Dissolve all unselected points"},
        {0, NULL, 0, NULL, NULL},
    };
  
    /* identifiers */
    ot->name = "Dissolve";
    ot->idname = "GPENCIL_OT_dissolve";
    ot->description = "Delete selected points without splitting strokes";
  
    /* callbacks */
    ot->invoke = WM_menu_invoke;
    ot->exec = gpencil_dissolve_exec;
    ot->poll = gpencil_stroke_edit_poll;
  
    /* flags */
    ot->flag = OPTYPE_UNDO | OPTYPE_REGISTER;
  
    /* props */
    ot->prop = RNA_def_enum(ot->srna,
                            "type",
                            prop_gpencil_dissolve_types,
                            0,
                            "Type",
                            "Method used for dissolving Stroke points");
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Snapping Selection to Grid Operator
   * \{ */
  
  /* 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 bool gpencil_snap_poll(bContext *C)
  {
    ScrArea *area = CTX_wm_area(C);
    Object *ob = CTX_data_active_object(C);
  
    return (ob != NULL) && (ob->type == OB_GPENCIL) &&
           ((area != NULL) && (area->spacetype == SPACE_VIEW3D));
  }
  
  static int gpencil_snap_to_grid(bContext *C, wmOperator *UNUSED(op))
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    RegionView3D *rv3d = CTX_wm_region_data(C);
    View3D *v3d = CTX_wm_view3d(C);
    Scene *scene = CTX_data_scene(C);
    Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
    Object *obact = CTX_data_active_object(C);
    const float gridf = ED_view3d_grid_view_scale(scene, v3d, rv3d, NULL);
  
    LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) {
      /* only editable and visible layers are considered */
      if (BKE_gpencil_layer_is_editable(gpl) && (gpl->actframe != NULL)) {
        bGPDframe *gpf = gpl->actframe;
        float diff_mat[4][4];
  
        /* calculate difference matrix object */
        BKE_gpencil_parent_matrix_get(depsgraph, obact, gpl, diff_mat);
  
        LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
          bGPDspoint *pt;
          int i;
  
          /* skip strokes that are invalid for current view */
          if (ED_gpencil_stroke_can_use(C, gps) == false) {
            continue;
          }
          /* check if the color is editable */
          if (ED_gpencil_stroke_color_use(obact, gpl, gps) == false) {
            continue;
          }
  
          // TODO: 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) {
              /* apply parent transformations */
              float fpt[3];
              mul_v3_m4v3(fpt, diff_mat, &pt->x);
  
              fpt[0] = gridf * floorf(0.5f + fpt[0] / gridf);
              fpt[1] = gridf * floorf(0.5f + fpt[1] / gridf);
              fpt[2] = gridf * floorf(0.5f + fpt[2] / gridf);
  
              /* return data */
              copy_v3_v3(&pt->x, fpt);
              gpencil_apply_parent_point(depsgraph, obact, gpl, pt);
            }
          }
        }
      }
    }
  
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    DEG_id_tag_update(&obact->id, ID_RECALC_COPY_ON_WRITE);
    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 = gpencil_snap_to_grid;
    ot->poll = gpencil_snap_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Snapping Selection to Cursor Operator
   * \{ */
  
  static int gpencil_snap_to_cursor(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
  
    Scene *scene = CTX_data_scene(C);
    Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
    Object *obact = CTX_data_active_object(C);
  
    const bool use_offset = RNA_boolean_get(op->ptr, "use_offset");
    const float *cursor_global = scene->cursor.location;
  
    LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) {
      /* only editable and visible layers are considered */
      if (BKE_gpencil_layer_is_editable(gpl) && (gpl->actframe != NULL)) {
        bGPDframe *gpf = gpl->actframe;
        float diff_mat[4][4];
  
        /* calculate difference matrix */
        BKE_gpencil_parent_matrix_get(depsgraph, obact, gpl, diff_mat);
  
        LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
          bGPDspoint *pt;
          int i;
  
          /* skip strokes that are invalid for current view */
          if (ED_gpencil_stroke_can_use(C, gps) == false) {
            continue;
          }
          /* check if the color is editable */
          if (ED_gpencil_stroke_color_use(obact, gpl, gps) == false) {
            continue;
          }
          /* 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);
                gpencil_apply_parent_point(depsgraph, obact, gpl, pt);
              }
            }
          }
        }
      }
    }
  
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    DEG_id_tag_update(&obact->id, ID_RECALC_COPY_ON_WRITE);
    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 = gpencil_snap_to_cursor;
    ot->poll = gpencil_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");
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Snapping Cursor to Selection Operator
   * \{ */
  
  static int gpencil_snap_cursor_to_sel(bContext *C, wmOperator *UNUSED(op))
  {
    Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
    Object *obact = CTX_data_active_object(C);
    Object *ob_eval = DEG_get_evaluated_object(depsgraph, obact);
    bGPdata *gpd = (bGPdata *)ob_eval->data;
  
    Scene *scene = CTX_data_scene(C);
    View3D *v3d = CTX_wm_view3d(C);
  
    float *cursor = scene->cursor.location;
    float centroid[3] = {0.0f};
    float min[3], max[3];
    size_t count = 0;
  
    INIT_MINMAX(min, max);
  
    /* calculate midpoints from selected points */
    LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) {
      /* only editable and visible layers are considered */
      if (BKE_gpencil_layer_is_editable(gpl) && (gpl->actframe != NULL)) {
        bGPDframe *gpf = gpl->actframe;
        float diff_mat[4][4];
  
        /* calculate difference matrix */
        BKE_gpencil_parent_matrix_get(depsgraph, obact, gpl, diff_mat);
  
        LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
          bGPDspoint *pt;
          int i;
  
          /* skip strokes that are invalid for current view */
          if (ED_gpencil_stroke_can_use(C, gps) == false) {
            continue;
          }
          /* check if the color is editable */
          if (ED_gpencil_stroke_color_use(obact, gpl, gps) == false) {
            continue;
          }
          /* 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) {
              /* apply parent transformations */
              float fpt[3];
              mul_v3_m4v3(fpt, diff_mat, &pt->x);
  
              add_v3_v3(centroid, fpt);
              minmax_v3v3_v3(min, max, fpt);
  
              count++;
            }
          }
        }
      }
    }
  
    if (scene->toolsettings->transform_pivot_point == V3D_AROUND_CENTER_BOUNDS) {
      mid_v3_v3v3(cursor, min, max);
    }
    else { /* #V3D_AROUND_CENTER_MEDIAN. */
      zero_v3(cursor);
      if (count) {
        mul_v3_fl(centroid, 1.0f / (float)count);
        copy_v3_v3(cursor, centroid);
      }
    }
  
    DEG_id_tag_update(&scene->id, ID_RECALC_COPY_ON_WRITE);
    WM_event_add_notifier(C, NC_SPACE | ND_SPACE_VIEW3D, v3d);
  
    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 = gpencil_snap_cursor_to_sel;
    ot->poll = gpencil_snap_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Apply Layer Thickness Change to Strokes Operator
   * \{ */
  
  static int gpencil_stroke_apply_thickness_exec(bContext *C, wmOperator *UNUSED(op))
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    bGPDlayer *gpl = BKE_gpencil_layer_active_get(gpd);
  
    /* sanity checks */
    if (ELEM(NULL, gpd, gpl, gpl->frames.first)) {
      return OPERATOR_CANCELLED;
    }
  
    /* loop all strokes */
    LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) {
      LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
        /* Apply thickness */
        if ((gps->thickness == 0) && (gpl->line_change == 0)) {
          gps->thickness = gpl->thickness;
        }
        else {
          gps->thickness = gps->thickness + gpl->line_change;
        }
      }
    }
    /* clear value */
    gpl->thickness = 0.0f;
    gpl->line_change = 0;
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_apply_thickness(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Apply Stroke Thickness";
    ot->idname = "GPENCIL_OT_stroke_apply_thickness";
    ot->description = "Apply the thickness change of the layer to its strokes";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_apply_thickness_exec;
    ot->poll = gpencil_active_layer_poll;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Toggle Cyclic Operator
   * \{ */
  
  enum {
    GP_STROKE_CYCLIC_CLOSE = 1,
    GP_STROKE_CYCLIC_OPEN = 2,
    GP_STROKE_CYCLIC_TOGGLE = 3,
  };
  
  static int gpencil_stroke_cyclical_set_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    Object *ob = CTX_data_active_object(C);
  
    const int type = RNA_enum_get(op->ptr, "type");
    const bool geometry = RNA_boolean_get(op->ptr, "geometry");
    const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd);
    bGPDstroke *gps = NULL;
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    /* loop all selected strokes */
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      bGPDframe *init_gpf = (is_multiedit) ? gpl->frames.first : gpl->actframe;
  
      for (bGPDframe *gpf = init_gpf; gpf; gpf = gpf->next) {
        if ((gpf == gpl->actframe) || ((gpf->flag & GP_FRAME_SELECT) && (is_multiedit))) {
          if (gpf == NULL) {
            continue;
          }
  
          for (gps = gpf->strokes.first; gps; gps = gps->next) {
            MaterialGPencilStyle *gp_style = BKE_gpencil_material_settings(ob, gps->mat_nr + 1);
            /* skip strokes that are not selected or invalid for current view */
            if (((gps->flag & GP_STROKE_SELECT) == 0) ||
                ED_gpencil_stroke_can_use(C, gps) == false) {
              continue;
            }
            /* skip hidden or locked colors */
            if (!gp_style || (gp_style->flag & GP_MATERIAL_HIDE) ||
                (gp_style->flag & GP_MATERIAL_LOCKED)) {
              continue;
            }
  
            switch (type) {
              case GP_STROKE_CYCLIC_CLOSE:
                /* Close all (enable) */
                gps->flag |= GP_STROKE_CYCLIC;
                break;
              case GP_STROKE_CYCLIC_OPEN:
                /* Open all (disable) */
                gps->flag &= ~GP_STROKE_CYCLIC;
                break;
              case GP_STROKE_CYCLIC_TOGGLE:
                /* Just toggle flag... */
                gps->flag ^= GP_STROKE_CYCLIC;
                break;
              default:
                BLI_assert(0);
                break;
            }
  
            /* Create new geometry. */
            if ((gps->flag & GP_STROKE_CYCLIC) && (geometry)) {
              BKE_gpencil_stroke_close(gps);
            }
          }
  
          /* if not multiedit, exit loop*/
          if (!is_multiedit) {
            break;
          }
        }
      }
    }
    CTX_DATA_END;
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  /**
   * Similar to #CURVE_OT_cyclic_toggle or #MASK_OT_cyclic_toggle, but with
   * option to force opened/closed strokes instead of just toggle behavior.
   */
  void GPENCIL_OT_stroke_cyclical_set(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    static const EnumPropertyItem cyclic_type[] = {
        {GP_STROKE_CYCLIC_CLOSE, "CLOSE", 0, "Close all", ""},
        {GP_STROKE_CYCLIC_OPEN, "OPEN", 0, "Open all", ""},
        {GP_STROKE_CYCLIC_TOGGLE, "TOGGLE", 0, "Toggle", ""},
        {0, NULL, 0, NULL, NULL},
    };
  
    /* identifiers */
    ot->name = "Set Cyclical State";
    ot->idname = "GPENCIL_OT_stroke_cyclical_set";
    ot->description = "Close or open the selected stroke adding an edge from last to first point";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_cyclical_set_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    ot->prop = RNA_def_enum(ot->srna, "type", cyclic_type, GP_STROKE_CYCLIC_TOGGLE, "Type", "");
    prop = RNA_def_boolean(
        ot->srna, "geometry", false, "Create Geometry", "Create new geometry for closing stroke");
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Toggle Flat Caps Operator
   * \{ */
  
  enum {
    GP_STROKE_CAPS_TOGGLE_BOTH = 0,
    GP_STROKE_CAPS_TOGGLE_START = 1,
    GP_STROKE_CAPS_TOGGLE_END = 2,
    GP_STROKE_CAPS_TOGGLE_DEFAULT = 3,
  };
  
  static int gpencil_stroke_caps_set_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    Object *ob = CTX_data_active_object(C);
  
    const int type = RNA_enum_get(op->ptr, "type");
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    /* loop all selected strokes */
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      if (gpl->actframe == NULL) {
        continue;
      }
  
      for (bGPDstroke *gps = gpl->actframe->strokes.last; gps; gps = gps->prev) {
        MaterialGPencilStyle *gp_style = BKE_gpencil_material_settings(ob, gps->mat_nr + 1);
  
        /* skip strokes that are not selected or invalid for current view */
        if (((gps->flag & GP_STROKE_SELECT) == 0) || (ED_gpencil_stroke_can_use(C, gps) == false)) {
          continue;
        }
        /* skip hidden or locked colors */
        if (!gp_style || (gp_style->flag & GP_MATERIAL_HIDE) ||
            (gp_style->flag & GP_MATERIAL_LOCKED)) {
          continue;
        }
  
        if ((type == GP_STROKE_CAPS_TOGGLE_BOTH) || (type == GP_STROKE_CAPS_TOGGLE_START)) {
          ++gps->caps[0];
          if (gps->caps[0] >= GP_STROKE_CAP_MAX) {
            gps->caps[0] = GP_STROKE_CAP_ROUND;
          }
        }
        if ((type == GP_STROKE_CAPS_TOGGLE_BOTH) || (type == GP_STROKE_CAPS_TOGGLE_END)) {
          ++gps->caps[1];
          if (gps->caps[1] >= GP_STROKE_CAP_MAX) {
            gps->caps[1] = GP_STROKE_CAP_ROUND;
          }
        }
        if (type == GP_STROKE_CAPS_TOGGLE_DEFAULT) {
          gps->caps[0] = GP_STROKE_CAP_ROUND;
          gps->caps[1] = GP_STROKE_CAP_ROUND;
        }
      }
    }
    CTX_DATA_END;
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  /**
   * Change Stroke caps mode Rounded or Flat
   */
  void GPENCIL_OT_stroke_caps_set(wmOperatorType *ot)
  {
    static const EnumPropertyItem toggle_type[] = {
        {GP_STROKE_CAPS_TOGGLE_BOTH, "TOGGLE", 0, "Both", ""},
        {GP_STROKE_CAPS_TOGGLE_START, "START", 0, "Start", ""},
        {GP_STROKE_CAPS_TOGGLE_END, "END", 0, "End", ""},
        {GP_STROKE_CAPS_TOGGLE_DEFAULT, "TOGGLE", 0, "Default", "Set as default rounded"},
        {0, NULL, 0, NULL, NULL},
    };
  
    /* identifiers */
    ot->name = "Set Caps Mode";
    ot->idname = "GPENCIL_OT_stroke_caps_set";
    ot->description = "Change Stroke caps mode (rounded or flat)";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_caps_set_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    ot->prop = RNA_def_enum(ot->srna, "type", toggle_type, GP_STROKE_CAPS_TOGGLE_BOTH, "Type", "");
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Join Operator
   * \{ */
  
  /* Helper: flip stroke */
  static void gpencil_flip_stroke(bGPDstroke *gps)
  {
    int end = gps->totpoints - 1;
  
    for (int i = 0; i < gps->totpoints / 2; i++) {
      bGPDspoint *point, *point2;
      bGPDspoint pt;
  
      /* save first point */
      point = &gps->points[i];
      pt.x = point->x;
      pt.y = point->y;
      pt.z = point->z;
      pt.flag = point->flag;
      pt.pressure = point->pressure;
      pt.strength = point->strength;
      pt.time = point->time;
      copy_v4_v4(pt.vert_color, point->vert_color);
  
      /* replace first point with last point */
      point2 = &gps->points[end];
      point->x = point2->x;
      point->y = point2->y;
      point->z = point2->z;
      point->flag = point2->flag;
      point->pressure = point2->pressure;
      point->strength = point2->strength;
      point->time = point2->time;
      copy_v4_v4(point->vert_color, point2->vert_color);
  
      /* replace last point with first saved before */
      point = &gps->points[end];
      point->x = pt.x;
      point->y = pt.y;
      point->z = pt.z;
      point->flag = pt.flag;
      point->pressure = pt.pressure;
      point->strength = pt.strength;
      point->time = pt.time;
      copy_v4_v4(point->vert_color, pt.vert_color);
  
      end--;
    }
  }
  
  /* Helper: copy point between strokes */
  static void gpencil_stroke_copy_point(bGPDstroke *gps,
                                        MDeformVert *dvert,
                                        bGPDspoint *point,
                                        const float delta[3],
                                        float pressure,
                                        float strength,
                                        float deltatime)
  {
    bGPDspoint *newpoint;
  
    gps->points = MEM_reallocN(gps->points, sizeof(bGPDspoint) * (gps->totpoints + 1));
    if (gps->dvert != NULL) {
      gps->dvert = MEM_reallocN(gps->dvert, sizeof(MDeformVert) * (gps->totpoints + 1));
    }
    else {
      /* If destination has weight add weight to origin. */
      if (dvert != NULL) {
        gps->dvert = MEM_callocN(sizeof(MDeformVert) * (gps->totpoints + 1), __func__);
      }
    }
  
    gps->totpoints++;
    newpoint = &gps->points[gps->totpoints - 1];
  
    newpoint->x = point->x * delta[0];
    newpoint->y = point->y * delta[1];
    newpoint->z = point->z * delta[2];
    newpoint->flag = point->flag;
    newpoint->pressure = pressure;
    newpoint->strength = strength;
    newpoint->time = point->time + deltatime;
    copy_v4_v4(newpoint->vert_color, point->vert_color);
  
    if (gps->dvert != NULL) {
      MDeformVert *newdvert = &gps->dvert[gps->totpoints - 1];
  
      if (dvert != NULL) {
        newdvert->totweight = dvert->totweight;
        newdvert->dw = MEM_dupallocN(dvert->dw);
      }
      else {
        newdvert->totweight = 0;
        newdvert->dw = NULL;
      }
    }
  }
  
  /* Helper: join two strokes using the shortest distance (reorder stroke if necessary ) */
  static void gpencil_stroke_join_strokes(bGPDstroke *gps_a,
                                          bGPDstroke *gps_b,
                                          const bool leave_gaps)
  {
    bGPDspoint point;
    bGPDspoint *pt;
    int i;
    const float delta[3] = {1.0f, 1.0f, 1.0f};
    float deltatime = 0.0f;
  
    /* sanity checks */
    if (ELEM(NULL, gps_a, gps_b)) {
      return;
    }
  
    if ((gps_a->totpoints == 0) || (gps_b->totpoints == 0)) {
      return;
    }
  
    /* define start and end points of each stroke */
    float area[3], sb[3], ea[3], eb[3];
    pt = &gps_a->points[0];
    copy_v3_v3(area, &pt->x);
  
    pt = &gps_a->points[gps_a->totpoints - 1];
    copy_v3_v3(ea, &pt->x);
  
    pt = &gps_b->points[0];
    copy_v3_v3(sb, &pt->x);
  
    pt = &gps_b->points[gps_b->totpoints - 1];
    copy_v3_v3(eb, &pt->x);
  
    /* review if need flip stroke B */
    float ea_sb = len_squared_v3v3(ea, sb);
    float ea_eb = len_squared_v3v3(ea, eb);
    /* flip if distance to end point is shorter */
    if (ea_eb < ea_sb) {
      gpencil_flip_stroke(gps_b);
    }
  
    /* don't visibly link the first and last points? */
    if (leave_gaps) {
      /* 1st: add one tail point to start invisible area */
      point = gps_a->points[gps_a->totpoints - 1];
      deltatime = point.time;
  
      gpencil_stroke_copy_point(gps_a, NULL, &point, delta, 0.0f, 0.0f, 0.0f);
  
      /* 2nd: add one head point to finish invisible area */
      point = gps_b->points[0];
      gpencil_stroke_copy_point(gps_a, NULL, &point, delta, 0.0f, 0.0f, deltatime);
    }
  
    /* 3rd: add all points */
    for (i = 0, pt = gps_b->points; i < gps_b->totpoints && pt; i++, pt++) {
      MDeformVert *dvert = (gps_b->dvert) ? &gps_b->dvert[i] : NULL;
      gpencil_stroke_copy_point(gps_a, dvert, pt, delta, pt->pressure, pt->strength, deltatime);
    }
  }
  
  static int gpencil_stroke_join_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    bGPDlayer *activegpl = BKE_gpencil_layer_active_get(gpd);
    Object *ob = CTX_data_active_object(C);
  
    bGPDframe *gpf_a = NULL;
    bGPDstroke *stroke_a = NULL;
    bGPDstroke *stroke_b = NULL;
    bGPDstroke *new_stroke = NULL;
  
    const int type = RNA_enum_get(op->ptr, "type");
    const bool leave_gaps = RNA_boolean_get(op->ptr, "leave_gaps");
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    if (activegpl->flag & GP_LAYER_LOCKED) {
      return OPERATOR_CANCELLED;
    }
  
    BLI_assert(ELEM(type, GP_STROKE_JOIN, GP_STROKE_JOINCOPY));
  
    /* read all selected strokes */
    bool first = false;
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      bGPDframe *gpf = gpl->actframe;
      if (gpf == NULL) {
        continue;
      }
  
      LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) {
        if (gps->flag & GP_STROKE_SELECT) {
          /* skip strokes that are invalid for current view */
          if (ED_gpencil_stroke_can_use(C, gps) == false) {
            continue;
          }
          /* check if the color is editable */
          if (ED_gpencil_stroke_color_use(ob, gpl, gps) == false) {
            continue;
          }
  
          /* to join strokes, cyclic must be disabled */
          gps->flag &= ~GP_STROKE_CYCLIC;
  
          /* saves first frame and stroke */
          if (!first) {
            first = true;
            gpf_a = gpf;
            stroke_a = gps;
          }
          else {
            stroke_b = gps;
  
            /* create a new stroke if was not created before (only created if something to join) */
            if (new_stroke == NULL) {
              new_stroke = BKE_gpencil_stroke_duplicate(stroke_a, true);
  
              /* if new, set current color */
              if (type == GP_STROKE_JOINCOPY) {
                new_stroke->mat_nr = stroke_a->mat_nr;
              }
            }
  
            /* join new_stroke and stroke B. New stroke will contain all the previous data */
            gpencil_stroke_join_strokes(new_stroke, stroke_b, leave_gaps);
  
            /* if join only, delete old strokes */
            if (type == GP_STROKE_JOIN) {
              if (stroke_a) {
                /* Calc geometry data. */
                BKE_gpencil_stroke_geometry_update(new_stroke);
  
                BLI_insertlinkbefore(&gpf_a->strokes, stroke_a, new_stroke);
                BLI_remlink(&gpf->strokes, stroke_a);
                BKE_gpencil_free_stroke(stroke_a);
                stroke_a = NULL;
              }
              if (stroke_b) {
                BLI_remlink(&gpf->strokes, stroke_b);
                BKE_gpencil_free_stroke(stroke_b);
                stroke_b = NULL;
              }
            }
          }
        }
      }
    }
    CTX_DATA_END;
  
    /* add new stroke if was not added before */
    if (type == GP_STROKE_JOINCOPY) {
      if (new_stroke) {
        /* Add a new frame if needed */
        if (activegpl->actframe == NULL) {
          activegpl->actframe = BKE_gpencil_frame_addnew(activegpl, gpf_a->framenum);
        }
        /* Calc geometry data. */
        BKE_gpencil_stroke_geometry_update(new_stroke);
  
        BLI_addtail(&activegpl->actframe->strokes, new_stroke);
      }
    }
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_join(wmOperatorType *ot)
  {
    static const EnumPropertyItem join_type[] = {
        {GP_STROKE_JOIN, "JOIN", 0, "Join", ""},
        {GP_STROKE_JOINCOPY, "JOINCOPY", 0, "Join and Copy", ""},
        {0, NULL, 0, NULL, NULL},
    };
  
    /* identifiers */
    ot->name = "Join Strokes";
    ot->idname = "GPENCIL_OT_stroke_join";
    ot->description = "Join selected strokes (optionally as new stroke)";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_join_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    ot->prop = RNA_def_enum(ot->srna, "type", join_type, GP_STROKE_JOIN, "Type", "");
    RNA_def_boolean(ot->srna,
                    "leave_gaps",
                    false,
                    "Leave Gaps",
                    "Leave gaps between joined strokes instead of linking them");
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Flip Operator
   * \{ */
  
  static int gpencil_stroke_flip_exec(bContext *C, wmOperator *UNUSED(op))
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    Object *ob = CTX_data_active_object(C);
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    /* read all selected strokes */
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      bGPDframe *gpf = gpl->actframe;
      if (gpf == NULL) {
        continue;
      }
  
      LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
        if (gps->flag & GP_STROKE_SELECT) {
          /* skip strokes that are invalid for current view */
          if (ED_gpencil_stroke_can_use(C, gps) == false) {
            continue;
          }
          /* check if the color is editable */
          if (ED_gpencil_stroke_color_use(ob, gpl, gps) == false) {
            continue;
          }
  
          /* flip stroke */
          gpencil_flip_stroke(gps);
        }
      }
    }
    CTX_DATA_END;
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_flip(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Flip Stroke";
    ot->idname = "GPENCIL_OT_stroke_flip";
    ot->description = "Change direction of the points of the selected strokes";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_flip_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Re-project Operator
   * \{ */
  
  static int gpencil_strokes_reproject_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    Scene *scene = CTX_data_scene(C);
    Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C);
    ARegion *region = CTX_wm_region(C);
    int oldframe = (int)DEG_get_ctime(depsgraph);
    const eGP_ReprojectModes mode = RNA_enum_get(op->ptr, "type");
    const bool keep_original = RNA_boolean_get(op->ptr, "keep_original");
  
    /* Init space conversion stuff. */
    GP_SpaceConversion gsc = {NULL};
    SnapObjectContext *sctx = NULL;
    gpencil_point_conversion_init(C, &gsc);
    /* Init snap context for geometry projection. */
    sctx = ED_transform_snap_object_context_create_view3d(scene, 0, region, CTX_wm_view3d(C));
  
    int cfra_prv = INT_MIN;
  
    /* Go through each editable + selected stroke, adjusting each of its points one by one... */
    GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) {
      if (gps->flag & GP_STROKE_SELECT) {
  
        /* update frame to get the new location of objects */
        if ((mode == GP_REPROJECT_SURFACE) && (cfra_prv != gpf_->framenum)) {
          cfra_prv = gpf_->framenum;
          CFRA = gpf_->framenum;
          BKE_scene_graph_update_for_newframe(depsgraph);
        }
  
        ED_gpencil_stroke_reproject(depsgraph, &gsc, sctx, gpl, gpf_, gps, mode, keep_original);
      }
    }
    GP_EDITABLE_STROKES_END(gpstroke_iter);
  
    /* return frame state and DB to original state */
    CFRA = oldframe;
    BKE_scene_graph_update_for_newframe(depsgraph);
  
    if (sctx != NULL) {
      ED_transform_snap_object_context_destroy(sctx);
    }
  
    /* update changed data */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_reproject(wmOperatorType *ot)
  {
    static const EnumPropertyItem reproject_type[] = {
        {GP_REPROJECT_FRONT, "FRONT", 0, "Front", "Reproject the strokes using the X-Z plane"},
        {GP_REPROJECT_SIDE, "SIDE", 0, "Side", "Reproject the strokes using the Y-Z plane"},
        {GP_REPROJECT_TOP, "TOP", 0, "Top", "Reproject the strokes using the X-Y plane"},
        {GP_REPROJECT_VIEW,
         "VIEW",
         0,
         "View",
         "Reproject the strokes to end up on the same plane, as if drawn from the current viewpoint "
         "using 'Cursor' Stroke Placement"},
        {GP_REPROJECT_SURFACE,
         "SURFACE",
         0,
         "Surface",
         "Reproject the strokes on to the scene geometry, as if drawn using 'Surface' placement"},
        {GP_REPROJECT_CURSOR,
         "CURSOR",
         0,
         "Cursor",
         "Reproject the strokes using the orientation of 3D cursor"},
        {0, NULL, 0, NULL, NULL},
    };
  
    /* identifiers */
    ot->name = "Reproject Strokes";
    ot->idname = "GPENCIL_OT_reproject";
    ot->description =
        "Reproject the selected strokes from the current viewpoint as if they had been newly drawn "
        "(e.g. to fix problems from accidental 3D cursor movement or accidental viewport changes, "
        "or for matching deforming geometry)";
  
    /* callbacks */
    ot->invoke = WM_menu_invoke;
    ot->exec = gpencil_strokes_reproject_exec;
    ot->poll = gpencil_strokes_edit3d_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    ot->prop = RNA_def_enum(
        ot->srna, "type", reproject_type, GP_REPROJECT_VIEW, "Projection Type", "");
  
    RNA_def_boolean(
        ot->srna,
        "keep_original",
        0,
        "Keep Original",
        "Keep original strokes and create a copy before reprojecting instead of reproject them");
  }
  
  static int gpencil_recalc_geometry_exec(bContext *C, wmOperator *UNUSED(op))
  {
    Object *ob = CTX_data_active_object(C);
    if ((ob == NULL) || (ob->type != OB_GPENCIL)) {
      return OPERATOR_CANCELLED;
    }
  
    bGPdata *gpd = (bGPdata *)ob->data;
    LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) {
      LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) {
        LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
          BKE_gpencil_stroke_geometry_update(gps);
        }
      }
    }
    /* update changed data */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_recalc_geometry(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Recalculate internal geometry";
    ot->idname = "GPENCIL_OT_recalc_geometry";
    ot->description = "Update all internal geometry data";
  
    /* callbacks */
    ot->exec = gpencil_recalc_geometry_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Subdivide Operator
   * \{ */
  
  /* helper to smooth */
  static void gpencil_smooth_stroke(bContext *C, wmOperator *op)
  {
    const int repeat = RNA_int_get(op->ptr, "repeat");
    float factor = RNA_float_get(op->ptr, "factor");
    const bool only_selected = RNA_boolean_get(op->ptr, "only_selected");
    const bool smooth_position = RNA_boolean_get(op->ptr, "smooth_position");
    const bool smooth_thickness = RNA_boolean_get(op->ptr, "smooth_thickness");
    const bool smooth_strength = RNA_boolean_get(op->ptr, "smooth_strength");
    const bool smooth_uv = RNA_boolean_get(op->ptr, "smooth_uv");
  
    if (factor == 0.0f) {
      return;
    }
  
    GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) {
      if (gps->flag & GP_STROKE_SELECT) {
        for (int r = 0; r < repeat; r++) {
          for (int i = 0; i < gps->totpoints; i++) {
            bGPDspoint *pt = &gps->points[i];
            if ((only_selected) && ((pt->flag & GP_SPOINT_SELECT) == 0)) {
              continue;
            }
  
            /* perform smoothing */
            if (smooth_position) {
              BKE_gpencil_stroke_smooth(gps, i, factor);
            }
            if (smooth_strength) {
              BKE_gpencil_stroke_smooth_strength(gps, i, factor);
            }
            if (smooth_thickness) {
              /* thickness need to repeat process several times */
              for (int r2 = 0; r2 < r * 20; r2++) {
                BKE_gpencil_stroke_smooth_thickness(gps, i, factor);
              }
            }
            if (smooth_uv) {
              BKE_gpencil_stroke_smooth_uv(gps, i, factor);
            }
          }
        }
      }
    }
    GP_EDITABLE_STROKES_END(gpstroke_iter);
  }
  
  /* helper: Count how many points need to be inserted */
  static int gpencil_count_subdivision_cuts(bGPDstroke *gps)
  {
    bGPDspoint *pt;
    int i;
    int totnewpoints = 0;
    for (i = 0, pt = gps->points; i < gps->totpoints && pt; i++, pt++) {
      if (pt->flag & GP_SPOINT_SELECT) {
        if (i + 1 < gps->totpoints) {
          if (gps->points[i + 1].flag & GP_SPOINT_SELECT) {
            totnewpoints++;
          }
        }
      }
    }
  
    return totnewpoints;
  }
  
  static int gpencil_stroke_subdivide_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    bGPDspoint *temp_points;
    const int cuts = RNA_int_get(op->ptr, "number_cuts");
  
    int totnewpoints, oldtotpoints;
    int i2;
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    /* Go through each editable + selected stroke */
    GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) {
      if (gps->flag & GP_STROKE_SELECT) {
        /* loop as many times as cuts */
        for (int s = 0; s < cuts; s++) {
          totnewpoints = gpencil_count_subdivision_cuts(gps);
          if (totnewpoints == 0) {
            continue;
          }
          /* duplicate points in a temp area */
          temp_points = MEM_dupallocN(gps->points);
          oldtotpoints = gps->totpoints;
  
          MDeformVert *temp_dverts = NULL;
          MDeformVert *dvert_final = NULL;
          MDeformVert *dvert = NULL;
          MDeformVert *dvert_next = NULL;
          if (gps->dvert != NULL) {
            temp_dverts = MEM_dupallocN(gps->dvert);
          }
  
          /* resize the points arrays */
          gps->totpoints += totnewpoints;
          gps->points = MEM_recallocN(gps->points, sizeof(*gps->points) * gps->totpoints);
          if (gps->dvert != NULL) {
            gps->dvert = MEM_recallocN(gps->dvert, sizeof(*gps->dvert) * gps->totpoints);
          }
  
          /* loop and interpolate */
          i2 = 0;
          for (int i = 0; i < oldtotpoints; i++) {
            bGPDspoint *pt = &temp_points[i];
            bGPDspoint *pt_final = &gps->points[i2];
  
            /* copy current point */
            copy_v3_v3(&pt_final->x, &pt->x);
            pt_final->pressure = pt->pressure;
            pt_final->strength = pt->strength;
            pt_final->time = pt->time;
            pt_final->flag = pt->flag;
            copy_v4_v4(pt_final->vert_color, pt->vert_color);
  
            if (gps->dvert != NULL) {
              dvert = &temp_dverts[i];
              dvert_final = &gps->dvert[i2];
              dvert_final->totweight = dvert->totweight;
              dvert_final->dw = dvert->dw;
            }
            i2++;
  
            /* if next point is selected add a half way point */
            if (pt->flag & GP_SPOINT_SELECT) {
              if (i + 1 < oldtotpoints) {
                if (temp_points[i + 1].flag & GP_SPOINT_SELECT) {
                  pt_final = &gps->points[i2];
                  if (gps->dvert != NULL) {
                    dvert_final = &gps->dvert[i2];
                  }
                  /* Interpolate all values */
                  bGPDspoint *next = &temp_points[i + 1];
                  interp_v3_v3v3(&pt_final->x, &pt->x, &next->x, 0.5f);
                  pt_final->pressure = interpf(pt->pressure, next->pressure, 0.5f);
                  pt_final->strength = interpf(pt->strength, next->strength, 0.5f);
                  CLAMP(pt_final->strength, GPENCIL_STRENGTH_MIN, 1.0f);
                  interp_v4_v4v4(pt_final->vert_color, pt->vert_color, next->vert_color, 0.5f);
                  pt_final->time = interpf(pt->time, next->time, 0.5f);
                  pt_final->flag |= GP_SPOINT_SELECT;
  
                  /* interpolate weights */
                  if (gps->dvert != NULL) {
                    dvert = &temp_dverts[i];
                    dvert_next = &temp_dverts[i + 1];
                    dvert_final = &gps->dvert[i2];
  
                    dvert_final->totweight = dvert->totweight;
                    dvert_final->dw = MEM_dupallocN(dvert->dw);
  
                    /* interpolate weight values */
                    for (int d = 0; d < dvert->totweight; d++) {
                      MDeformWeight *dw_a = &dvert->dw[d];
                      if (dvert_next->totweight > d) {
                        MDeformWeight *dw_b = &dvert_next->dw[d];
                        MDeformWeight *dw_final = &dvert_final->dw[d];
                        dw_final->weight = interpf(dw_a->weight, dw_b->weight, 0.5f);
                      }
                    }
                  }
  
                  i2++;
                }
              }
            }
          }
          /* free temp memory */
          MEM_SAFE_FREE(temp_points);
          MEM_SAFE_FREE(temp_dverts);
        }
  
        /* Calc geometry data. */
        BKE_gpencil_stroke_geometry_update(gps);
      }
    }
    GP_EDITABLE_STROKES_END(gpstroke_iter);
  
    /* smooth stroke */
    gpencil_smooth_stroke(C, op);
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_subdivide(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Subdivide Stroke";
    ot->idname = "GPENCIL_OT_stroke_subdivide";
    ot->description =
        "Subdivide between continuous selected points of the stroke adding a point half way between "
        "them";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_subdivide_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    prop = RNA_def_int(ot->srna, "number_cuts", 1, 1, 10, "Number of Cuts", "", 1, 5);
    /* avoid re-using last var because it can cause _very_ high value and annoy users */
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  
    /* Smooth parameters */
    RNA_def_float(ot->srna, "factor", 0.0f, 0.0f, 2.0f, "Smooth", "", 0.0f, 2.0f);
    prop = RNA_def_int(ot->srna, "repeat", 1, 1, 10, "Repeat", "", 1, 5);
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
    RNA_def_boolean(ot->srna,
                    "only_selected",
                    true,
                    "Selected Points",
                    "Smooth only selected points in the stroke");
    RNA_def_boolean(ot->srna, "smooth_position", true, "Position", "");
    RNA_def_boolean(ot->srna, "smooth_thickness", true, "Thickness", "");
    RNA_def_boolean(ot->srna, "smooth_strength", false, "Strength", "");
    RNA_def_boolean(ot->srna, "smooth_uv", false, "UV", "");
  }
  
  /* ** simplify stroke *** */
  static int gpencil_stroke_simplify_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    float factor = RNA_float_get(op->ptr, "factor");
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    /* Go through each editable + selected stroke */
    GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) {
      if (gps->flag & GP_STROKE_SELECT) {
        /* simplify stroke using Ramer-Douglas-Peucker algorithm */
        BKE_gpencil_stroke_simplify_adaptive(gps, factor);
      }
    }
    GP_EDITABLE_STROKES_END(gpstroke_iter);
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_simplify(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Simplify Stroke";
    ot->idname = "GPENCIL_OT_stroke_simplify";
    ot->description = "Simplify selected stroked reducing number of points";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_simplify_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    prop = RNA_def_float(ot->srna, "factor", 0.0f, 0.0f, 100.0f, "Factor", "", 0.0f, 100.0f);
    /* avoid re-using last var */
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  }
  
  /* ** simplify stroke using fixed algorithm *** */
  static int gpencil_stroke_simplify_fixed_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    int steps = RNA_int_get(op->ptr, "step");
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    /* Go through each editable + selected stroke */
    GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) {
      if (gps->flag & GP_STROKE_SELECT) {
        for (int i = 0; i < steps; i++) {
          BKE_gpencil_stroke_simplify_fixed(gps);
        }
      }
    }
    GP_EDITABLE_STROKES_END(gpstroke_iter);
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_simplify_fixed(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Simplify Fixed Stroke";
    ot->idname = "GPENCIL_OT_stroke_simplify_fixed";
    ot->description = "Simplify selected stroked reducing number of points using fixed algorithm";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_simplify_fixed_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    prop = RNA_def_int(ot->srna, "step", 1, 1, 100, "Steps", "Number of simplify steps", 1, 10);
  
    /* avoid re-using last var */
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  }
  
  /* ** Resample stroke *** */
  static int gpencil_stroke_sample_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    const float length = RNA_float_get(op->ptr, "length");
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    /* Go through each editable + selected stroke */
    GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) {
      if (gps->flag & GP_STROKE_SELECT) {
        BKE_gpencil_stroke_sample(gps, length, true);
      }
    }
    GP_EDITABLE_STROKES_END(gpstroke_iter);
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_sample(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Sample Stroke";
    ot->idname = "GPENCIL_OT_stroke_sample";
    ot->description = "Sample stroke points to predefined segment length";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_sample_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    prop = RNA_def_float(ot->srna, "length", 0.1f, 0.0f, 100.0f, "Length", "", 0.0f, 100.0f);
    /* avoid re-using last var */
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Trim Operator
   * \{ */
  
  static int gpencil_stroke_trim_exec(bContext *C, wmOperator *UNUSED(op))
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    /* Go through each editable + selected stroke */
    const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd);
  
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      bGPDframe *init_gpf = (is_multiedit) ? gpl->frames.first : gpl->actframe;
  
      for (bGPDframe *gpf = init_gpf; gpf; gpf = gpf->next) {
        if ((gpf == gpl->actframe) || ((gpf->flag & GP_FRAME_SELECT) && (is_multiedit))) {
  
          if (gpf == NULL) {
            continue;
          }
  
          LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) {
  
            /* skip strokes that are invalid for current view */
            if (ED_gpencil_stroke_can_use(C, gps) == false) {
              continue;
            }
  
            if (gps->flag & GP_STROKE_SELECT) {
              BKE_gpencil_stroke_trim(gps);
            }
          }
          /* if not multiedit, exit loop*/
          if (!is_multiedit) {
            break;
          }
        }
      }
    }
    CTX_DATA_END;
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_trim(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Trim Stroke";
    ot->idname = "GPENCIL_OT_stroke_trim";
    ot->description = "Trim selected stroke to first loop or intersection";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_trim_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Separate Operator
   * \{ */
  
  typedef enum eGP_SeparateModes {
    /* Points */
    GP_SEPARATE_POINT = 0,
    /* Selected Strokes */
    GP_SEPARATE_STROKE,
    /* Current Layer */
    GP_SEPARATE_LAYER,
  } eGP_SeparateModes;
  
  static int gpencil_stroke_separate_exec(bContext *C, wmOperator *op)
  {
    Base *base_new;
    Main *bmain = CTX_data_main(C);
    Scene *scene = CTX_data_scene(C);
    ViewLayer *view_layer = CTX_data_view_layer(C);
    Base *base_prev = CTX_data_active_base(C);
    bGPdata *gpd_src = ED_gpencil_data_get_active(C);
    Object *ob = CTX_data_active_object(C);
  
    Object *ob_dst = NULL;
    bGPdata *gpd_dst = NULL;
    bGPDlayer *gpl_dst = NULL;
    bGPDframe *gpf_dst = NULL;
    bGPDspoint *pt;
    Material *ma = NULL;
    int i, idx;
  
    eGP_SeparateModes mode = RNA_enum_get(op->ptr, "mode");
  
    /* sanity checks */
    if (ELEM(NULL, gpd_src)) {
      return OPERATOR_CANCELLED;
    }
  
    if ((mode == GP_SEPARATE_LAYER) && (BLI_listbase_count(&gpd_src->layers) == 1)) {
      BKE_report(op->reports, RPT_ERROR, "Cannot separate an object with one layer only");
      return OPERATOR_CANCELLED;
    }
  
    const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd_src);
  
    /* Create a new object. */
    /* Take into account user preferences for duplicating actions. */
    const eDupli_ID_Flags dupflag = (U.dupflag & USER_DUP_ACT);
  
    base_new = ED_object_add_duplicate(bmain, scene, view_layer, base_prev, dupflag);
    ob_dst = base_new->object;
    ob_dst->mode = OB_MODE_OBJECT;
    /* Duplication will increment #bGPdata user-count, but since we create a new grease-pencil
     * data-block for ob_dst (which gets its own user automatically),
     * we have to decrement the user-count again. */
    gpd_dst = BKE_gpencil_data_addnew(bmain, gpd_src->id.name + 2);
    id_us_min(ob_dst->data);
    ob_dst->data = (bGPdata *)gpd_dst;
  
    /* Loop old data-block and separate parts. */
    if ((mode == GP_SEPARATE_POINT) || (mode == GP_SEPARATE_STROKE)) {
      CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
        gpl_dst = NULL;
        bGPDframe *init_gpf = (is_multiedit) ? gpl->frames.first : gpl->actframe;
  
        for (bGPDframe *gpf = init_gpf; gpf; gpf = gpf->next) {
          if ((gpf == gpl->actframe) || ((gpf->flag & GP_FRAME_SELECT) && (is_multiedit))) {
  
            if (gpf == NULL) {
              continue;
            }
  
            gpf_dst = NULL;
  
            LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) {
  
              /* skip strokes that are invalid for current view */
              if (ED_gpencil_stroke_can_use(C, gps) == false) {
                continue;
              }
              /* check if the color is editable */
              if (ED_gpencil_stroke_color_use(ob, gpl, gps) == false) {
                continue;
              }
              /* Separate selected strokes. */
              if (gps->flag & GP_STROKE_SELECT) {
                /* add layer if not created before */
                if (gpl_dst == NULL) {
                  gpl_dst = BKE_gpencil_layer_addnew(gpd_dst, gpl->info, false);
                }
  
                /* add frame if not created before */
                if (gpf_dst == NULL) {
                  gpf_dst = BKE_gpencil_layer_frame_get(gpl_dst, gpf->framenum, GP_GETFRAME_ADD_NEW);
                }
  
                /* add duplicate materials */
  
                /* XXX same material can be in multiple slots. */
                ma = BKE_gpencil_material(ob, gps->mat_nr + 1);
  
                idx = BKE_gpencil_object_material_ensure(bmain, ob_dst, ma);
  
                /* selected points mode */
                if (mode == GP_SEPARATE_POINT) {
                  /* make copy of source stroke */
                  bGPDstroke *gps_dst = BKE_gpencil_stroke_duplicate(gps, true);
  
                  /* Reassign material. */
                  gps_dst->mat_nr = idx;
  
                  /* link to destination frame */
                  BLI_addtail(&gpf_dst->strokes, gps_dst);
  
                  /* Invert selection status of all points in destination stroke */
                  for (i = 0, pt = gps_dst->points; i < gps_dst->totpoints; i++, pt++) {
                    pt->flag ^= GP_SPOINT_SELECT;
                  }
  
                  /* delete selected points from destination stroke */
                  gpencil_stroke_delete_tagged_points(
                      gpf_dst, gps_dst, NULL, GP_SPOINT_SELECT, false, 0);
  
                  /* delete selected points from origin stroke */
                  gpencil_stroke_delete_tagged_points(
                      gpf, gps, gps->next, GP_SPOINT_SELECT, false, 0);
                }
                /* selected strokes mode */
                else if (mode == GP_SEPARATE_STROKE) {
                  /* deselect old stroke */
                  gps->flag &= ~GP_STROKE_SELECT;
                  /* unlink from source frame */
                  BLI_remlink(&gpf->strokes, gps);
                  gps->prev = gps->next = NULL;
                  /* relink to destination frame */
                  BLI_addtail(&gpf_dst->strokes, gps);
                  /* Reassign material. */
                  gps->mat_nr = idx;
                }
              }
            }
          }
  
          /* if not multiedit, exit loop*/
          if (!is_multiedit) {
            break;
          }
        }
      }
      CTX_DATA_END;
    }
    else if (mode == GP_SEPARATE_LAYER) {
      bGPDlayer *gpl = CTX_data_active_gpencil_layer(C);
      if (gpl) {
        /* try to set a new active layer in source datablock */
        if (gpl->prev) {
          BKE_gpencil_layer_active_set(gpd_src, gpl->prev);
        }
        else if (gpl->next) {
          BKE_gpencil_layer_active_set(gpd_src, gpl->next);
        }
        /* unlink from source datablock */
        BLI_remlink(&gpd_src->layers, gpl);
        gpl->prev = gpl->next = NULL;
        /* relink to destination datablock */
        BLI_addtail(&gpd_dst->layers, gpl);
  
        /* add duplicate materials */
        LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) {
          LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
            /* skip strokes that are invalid for current view */
            if (ED_gpencil_stroke_can_use(C, gps) == false) {
              continue;
            }
            ma = BKE_gpencil_material(ob, gps->mat_nr + 1);
            gps->mat_nr = BKE_gpencil_object_material_ensure(bmain, ob_dst, ma);
          }
        }
      }
    }
  
    /* Ensure destination object has one active layer. */
    if (gpd_dst->layers.first != NULL) {
      if (BKE_gpencil_layer_active_get(gpd_dst) == NULL) {
        BKE_gpencil_layer_active_set(gpd_dst, gpd_dst->layers.first);
      }
    }
  
    /* Remove unused slots. */
    int actcol = ob_dst->actcol;
    for (int slot = 1; slot <= ob_dst->totcol; slot++) {
      while (slot <= ob_dst->totcol && !BKE_object_material_slot_used(ob_dst->data, slot)) {
        ob_dst->actcol = slot;
        BKE_object_material_slot_remove(bmain, ob_dst);
        if (actcol >= slot) {
          actcol--;
        }
      }
    }
    ob_dst->actcol = actcol;
  
    DEG_id_tag_update(&gpd_src->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    DEG_id_tag_update(&gpd_dst->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
  
    DEG_relations_tag_update(bmain);
    WM_event_add_notifier(C, NC_OBJECT | ND_DRAW, NULL);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_SELECTED, NULL);
    ED_outliner_select_sync_from_object_tag(C);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_separate(wmOperatorType *ot)
  {
    static const EnumPropertyItem separate_type[] = {
        {GP_SEPARATE_POINT, "POINT", 0, "Selected Points", "Separate the selected points"},
        {GP_SEPARATE_STROKE, "STROKE", 0, "Selected Strokes", "Separate the selected strokes"},
        {GP_SEPARATE_LAYER, "LAYER", 0, "Active Layer", "Separate the strokes of the current layer"},
        {0, NULL, 0, NULL, NULL},
    };
  
    /* identifiers */
    ot->name = "Separate Strokes";
    ot->idname = "GPENCIL_OT_stroke_separate";
    ot->description = "Separate the selected strokes or layer in a new grease pencil object";
  
    /* callbacks */
    ot->invoke = WM_menu_invoke;
    ot->exec = gpencil_stroke_separate_exec;
    ot->poll = gpencil_strokes_edit3d_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    ot->prop = RNA_def_enum(ot->srna, "mode", separate_type, GP_SEPARATE_POINT, "Mode", "");
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Split Operator
   * \{ */
  
  static int gpencil_stroke_split_exec(bContext *C, wmOperator *UNUSED(op))
  {
    Object *ob = CTX_data_active_object(C);
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    bGPDspoint *pt;
    int i;
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
    const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd);
  
    /* loop strokes and split parts */
    CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) {
      bGPDframe *init_gpf = (is_multiedit) ? gpl->frames.first : gpl->actframe;
  
      for (bGPDframe *gpf = init_gpf; gpf; gpf = gpf->next) {
        if ((gpf == gpl->actframe) || ((gpf->flag & GP_FRAME_SELECT) && (is_multiedit))) {
  
          if (gpf == NULL) {
            continue;
          }
  
          LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) {
  
            /* skip strokes that are invalid for current view */
            if (ED_gpencil_stroke_can_use(C, gps) == false) {
              continue;
            }
            /* check if the color is editable */
            if (ED_gpencil_stroke_color_use(ob, gpl, gps) == false) {
              continue;
            }
            /* Split selected strokes. */
            if (gps->flag & GP_STROKE_SELECT) {
              /* make copy of source stroke */
              bGPDstroke *gps_dst = BKE_gpencil_stroke_duplicate(gps, true);
  
              /* link to same frame */
              BLI_addtail(&gpf->strokes, gps_dst);
  
              /* invert selection status of all points in destination stroke */
              for (i = 0, pt = gps_dst->points; i < gps_dst->totpoints; i++, pt++) {
                pt->flag ^= GP_SPOINT_SELECT;
              }
  
              /* delete selected points from destination stroke */
              gpencil_stroke_delete_tagged_points(gpf, gps_dst, NULL, GP_SPOINT_SELECT, true, 0);
  
              /* delete selected points from origin stroke */
              gpencil_stroke_delete_tagged_points(gpf, gps, gps->next, GP_SPOINT_SELECT, false, 0);
            }
          }
          /* select again tagged points */
          LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) {
            bGPDspoint *ptn = gps->points;
            for (int i2 = 0; i2 < gps->totpoints; i2++, ptn++) {
              if (ptn->flag & GP_SPOINT_TAG) {
                ptn->flag |= GP_SPOINT_SELECT;
                ptn->flag &= ~GP_SPOINT_TAG;
              }
            }
          }
        }
  
        /* if not multiedit, exit loop*/
        if (!is_multiedit) {
          break;
        }
      }
    }
    CTX_DATA_END;
  
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
  
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_split(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Split Strokes";
    ot->idname = "GPENCIL_OT_stroke_split";
    ot->description = "Split selected points as new stroke on same frame";
  
    /* callbacks */
    ot->exec = gpencil_stroke_split_exec;
    ot->poll = gpencil_strokes_edit3d_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Smooth Operator
   * \{ */
  
  static int gpencil_stroke_smooth_exec(bContext *C, wmOperator *op)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    gpencil_smooth_stroke(C, op);
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_smooth(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Smooth Stroke";
    ot->idname = "GPENCIL_OT_stroke_smooth";
    ot->description = "Smooth selected strokes";
  
    /* api callbacks */
    ot->exec = gpencil_stroke_smooth_exec;
    ot->poll = gpencil_active_layer_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    prop = RNA_def_int(ot->srna, "repeat", 1, 1, 50, "Repeat", "", 1, 20);
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  
    RNA_def_float(ot->srna, "factor", 0.5f, 0.0f, 2.0f, "Factor", "", 0.0f, 2.0f);
    RNA_def_boolean(ot->srna,
                    "only_selected",
                    true,
                    "Selected Points",
                    "Smooth only selected points in the stroke");
    RNA_def_boolean(ot->srna, "smooth_position", true, "Position", "");
    RNA_def_boolean(ot->srna, "smooth_thickness", true, "Thickness", "");
    RNA_def_boolean(ot->srna, "smooth_strength", false, "Strength", "");
    RNA_def_boolean(ot->srna, "smooth_uv", false, "UV", "");
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Stroke Cutter Operator
   * \{ */
  
  /* smart stroke cutter for trimming stroke ends */
  struct GP_SelectLassoUserData {
    rcti rect;
    const int (*mcoords)[2];
    int mcoords_len;
  };
  
  static bool gpencil_test_lasso(bGPDstroke *gps,
                                 bGPDspoint *pt,
                                 const GP_SpaceConversion *gsc,
                                 const float diff_mat[4][4],
                                 void *user_data)
  {
    const struct GP_SelectLassoUserData *data = user_data;
    bGPDspoint pt2;
    int x0, y0;
    gpencil_point_to_parent_space(pt, diff_mat, &pt2);
    gpencil_point_to_xy(gsc, gps, &pt2, &x0, &y0);
    /* test if in lasso */
    return ((!ELEM(V2D_IS_CLIPPED, x0, y0)) && BLI_rcti_isect_pt(&data->rect, x0, y0) &&
            BLI_lasso_is_point_inside(data->mcoords, data->mcoords_len, x0, y0, INT_MAX));
  }
  
  typedef bool (*GPencilTestFn)(bGPDstroke *gps,
                                bGPDspoint *pt,
                                const GP_SpaceConversion *gsc,
                                const float diff_mat[4][4],
                                void *user_data);
  
  static void gpencil_cutter_dissolve(bGPDlayer *hit_layer, bGPDstroke *hit_stroke)
  {
    bGPDspoint *pt = NULL;
    bGPDspoint *pt1 = NULL;
    int i;
  
    bGPDstroke *gpsn = hit_stroke->next;
  
    int totselect = 0;
    for (i = 0, pt = hit_stroke->points; i < hit_stroke->totpoints; i++, pt++) {
      if (pt->flag & GP_SPOINT_SELECT) {
        totselect++;
      }
    }
  
    /* if all points selected delete or only 2 points and 1 selected */
    if (((totselect == 1) && (hit_stroke->totpoints == 2)) || (hit_stroke->totpoints == totselect)) {
      BLI_remlink(&hit_layer->actframe->strokes, hit_stroke);
      BKE_gpencil_free_stroke(hit_stroke);
      hit_stroke = NULL;
    }
  
    /* if very small distance delete */
    if ((hit_stroke) && (hit_stroke->totpoints == 2)) {
      pt = &hit_stroke->points[0];
      pt1 = &hit_stroke->points[1];
      if (len_v3v3(&pt->x, &pt1->x) < 0.001f) {
        BLI_remlink(&hit_layer->actframe->strokes, hit_stroke);
        BKE_gpencil_free_stroke(hit_stroke);
        hit_stroke = NULL;
      }
    }
  
    if (hit_stroke) {
      /* tag and dissolve (untag new points) */
      for (i = 0, pt = hit_stroke->points; i < hit_stroke->totpoints; i++, pt++) {
        if (pt->flag & GP_SPOINT_SELECT) {
          pt->flag &= ~GP_SPOINT_SELECT;
          pt->flag |= GP_SPOINT_TAG;
        }
        else if (pt->flag & GP_SPOINT_TAG) {
          pt->flag &= ~GP_SPOINT_TAG;
        }
      }
      gpencil_stroke_delete_tagged_points(
          hit_layer->actframe, hit_stroke, gpsn, GP_SPOINT_TAG, false, 1);
    }
  }
  
  static int gpencil_cutter_lasso_select(bContext *C,
                                         wmOperator *op,
                                         GPencilTestFn is_inside_fn,
                                         void *user_data)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
    ScrArea *area = CTX_wm_area(C);
    ToolSettings *ts = CTX_data_tool_settings(C);
    const float scale = ts->gp_sculpt.isect_threshold;
  
    bGPDspoint *pt;
    GP_SpaceConversion gsc = {NULL};
  
    bool changed = false;
  
    /* sanity checks */
    if (area == NULL) {
      BKE_report(op->reports, RPT_ERROR, "No active area");
      return OPERATOR_CANCELLED;
    }
  
    /* init space conversion stuff */
    gpencil_point_conversion_init(C, &gsc);
  
    /* deselect all strokes first */
    CTX_DATA_BEGIN (C, bGPDstroke *, gps, editable_gpencil_strokes) {
      int i;
      for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) {
        pt->flag &= ~GP_SPOINT_SELECT;
      }
  
      gps->flag &= ~GP_STROKE_SELECT;
    }
    CTX_DATA_END;
  
    /* select points */
    GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) {
      int tot_inside = 0;
      const int oldtot = gps->totpoints;
      for (int i = 0; i < gps->totpoints; i++) {
        pt = &gps->points[i];
        if ((pt->flag & GP_SPOINT_SELECT) || (pt->flag & GP_SPOINT_TAG)) {
          continue;
        }
        /* convert point coords to screen-space */
        const bool is_inside = is_inside_fn(gps, pt, &gsc, gpstroke_iter.diff_mat, user_data);
        if (is_inside) {
          tot_inside++;
          changed = true;
          pt->flag |= GP_SPOINT_SELECT;
          gps->flag |= GP_STROKE_SELECT;
          float r_hita[3], r_hitb[3];
          if (gps->totpoints > 1) {
            ED_gpencil_select_stroke_segment(gpl, gps, pt, true, true, scale, r_hita, r_hitb);
          }
          /* avoid infinite loops */
          if (gps->totpoints > oldtot) {
            break;
          }
        }
      }
      /* if mark all points inside lasso set to remove all stroke */
      if ((tot_inside == oldtot) || ((tot_inside == 1) && (oldtot == 2))) {
        for (int i = 0; i < gps->totpoints; i++) {
          pt = &gps->points[i];
          pt->flag |= GP_SPOINT_SELECT;
        }
      }
    }
    GP_EDITABLE_STROKES_END(gpstroke_iter);
  
    /* dissolve selected points */
    LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) {
      if (gpl->flag & GP_LAYER_LOCKED) {
        continue;
      }
  
      bGPDframe *gpf = gpl->actframe;
      if (gpf == NULL) {
        continue;
      }
      LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) {
        if (gps->flag & GP_STROKE_SELECT) {
          gpencil_cutter_dissolve(gpl, gps);
        }
      }
    }
  
    /* updates */
    if (changed) {
      DEG_id_tag_update(&gpd->id, ID_RECALC_GEOMETRY | ID_RECALC_COPY_ON_WRITE);
      WM_event_add_notifier(C, NC_GPENCIL | NA_SELECTED, NULL);
      WM_event_add_notifier(C, NC_GEOM | ND_SELECT, NULL);
    }
  
    return OPERATOR_FINISHED;
  }
  
  static bool gpencil_cutter_poll(bContext *C)
  {
    bGPdata *gpd = ED_gpencil_data_get_active(C);
  
    if (GPENCIL_PAINT_MODE(gpd)) {
      if (gpd->layers.first) {
        return true;
      }
    }
  
    return false;
  }
  
  static int gpencil_cutter_exec(bContext *C, wmOperator *op)
  {
    ScrArea *area = CTX_wm_area(C);
    /* sanity checks */
    if (area == NULL) {
      BKE_report(op->reports, RPT_ERROR, "No active area");
      return OPERATOR_CANCELLED;
    }
  
    struct GP_SelectLassoUserData data = {0};
    data.mcoords = WM_gesture_lasso_path_to_array(C, op, &data.mcoords_len);
  
    /* Sanity check. */
    if (data.mcoords == NULL) {
      return OPERATOR_PASS_THROUGH;
    }
  
    /* Compute boundbox of lasso (for faster testing later). */
    BLI_lasso_boundbox(&data.rect, data.mcoords, data.mcoords_len);
  
    gpencil_cutter_lasso_select(C, op, gpencil_test_lasso, &data);
  
    MEM_freeN((void *)data.mcoords);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_cutter(wmOperatorType *ot)
  {
    /* identifiers */
    ot->name = "Stroke Cutter";
    ot->description = "Select section and cut";
    ot->idname = "GPENCIL_OT_stroke_cutter";
  
    /* callbacks */
    ot->invoke = WM_gesture_lasso_invoke;
    ot->modal = WM_gesture_lasso_modal;
    ot->exec = gpencil_cutter_exec;
    ot->poll = gpencil_cutter_poll;
    ot->cancel = WM_gesture_lasso_cancel;
  
    /* flag */
    ot->flag = OPTYPE_UNDO;
  
    /* properties */
    WM_operator_properties_gesture_lasso(ot);
  }
  
  bool ED_object_gpencil_exit(struct Main *bmain, Object *ob)
  {
    bool ok = false;
    if (ob) {
      bGPdata *gpd = (bGPdata *)ob->data;
  
      gpd->flag &= ~(GP_DATA_STROKE_PAINTMODE | GP_DATA_STROKE_EDITMODE | GP_DATA_STROKE_SCULPTMODE |
                     GP_DATA_STROKE_WEIGHTMODE | GP_DATA_STROKE_VERTEXMODE);
  
      ob->restore_mode = ob->mode;
      ob->mode &= ~(OB_MODE_PAINT_GPENCIL | OB_MODE_EDIT_GPENCIL | OB_MODE_SCULPT_GPENCIL |
                    OB_MODE_WEIGHT_GPENCIL | OB_MODE_VERTEX_GPENCIL);
  
      /* Inform all CoW versions that we changed the mode. */
      DEG_id_tag_update_ex(bmain, &ob->id, ID_RECALC_COPY_ON_WRITE);
      ok = true;
    }
    return ok;
  }
  
  /** \} */
  
  /* -------------------------------------------------------------------- */
  /** \name Merge By Distance Operator
   * \{ */
  
  static bool gpencil_merge_by_distance_poll(bContext *C)
  {
    Object *ob = CTX_data_active_object(C);
    if ((ob == NULL) || (ob->type != OB_GPENCIL)) {
      return false;
    }
    bGPdata *gpd = (bGPdata *)ob->data;
    if (gpd == NULL) {
      return false;
    }
  
    bGPDlayer *gpl = BKE_gpencil_layer_active_get(gpd);
  
    return ((gpl != NULL) && (ob->mode == OB_MODE_EDIT_GPENCIL));
  }
  
  static int gpencil_merge_by_distance_exec(bContext *C, wmOperator *op)
  {
    Object *ob = CTX_data_active_object(C);
    bGPdata *gpd = (bGPdata *)ob->data;
    const float threshold = RNA_float_get(op->ptr, "threshold");
    const bool unselected = RNA_boolean_get(op->ptr, "use_unselected");
  
    /* sanity checks */
    if (ELEM(NULL, gpd)) {
      return OPERATOR_CANCELLED;
    }
  
    /* Go through each editable selected stroke */
    GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) {
      if (gps->flag & GP_STROKE_SELECT) {
        BKE_gpencil_stroke_merge_distance(gpf_, gps, threshold, unselected);
      }
    }
    GP_EDITABLE_STROKES_END(gpstroke_iter);
  
    /* notifiers */
    DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
    WM_event_add_notifier(C, NC_GPENCIL | ND_DATA | NA_EDITED, NULL);
  
    return OPERATOR_FINISHED;
  }
  
  void GPENCIL_OT_stroke_merge_by_distance(wmOperatorType *ot)
  {
    PropertyRNA *prop;
  
    /* identifiers */
    ot->name = "Merge by Distance";
    ot->idname = "GPENCIL_OT_stroke_merge_by_distance";
    ot->description = "Merge points by distance";
  
    /* api callbacks */
    ot->exec = gpencil_merge_by_distance_exec;
    ot->poll = gpencil_merge_by_distance_poll;
  
    /* flags */
    ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
  
    /* properties */
    prop = RNA_def_float(ot->srna, "threshold", 0.001f, 0.0f, 100.0f, "Threshold", "", 0.0f, 100.0f);
    /* avoid re-using last var */
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  
    prop = RNA_def_boolean(
        ot->srna, "use_unselected", 0, "Unselected", "Use whole stroke, not only selected points");
    RNA_def_property_flag(prop, PROP_SKIP_SAVE);
  }
  
  /** \} */
  