/* SPDX-License-Identifier: GPL-2.0-or-later * Copyright 2008 Blender Foundation. */ /** \file * \ingroup edgpencil * Operators for editing Grease Pencil strokes. */ #include #include #include #include #include #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_material_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_deform.h" #include "BKE_global.h" #include "BKE_gpencil.h" #include "BKE_gpencil_curve.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_keyframing.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 * \{ */ /* 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; } static bool gpencil_stroke_not_in_curve_edit_mode(bContext *C) { Object *ob = CTX_data_active_object(C); if ((ob == NULL) || (ob->type != OB_GPENCIL)) { return false; } bGPdata *gpd = (bGPdata *)ob->data; bGPDlayer *gpl = BKE_gpencil_layer_active_get(gpd); return (gpl != NULL && !GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd)); } /** \} */ /* -------------------------------------------------------------------- */ /** \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; } /* Recalculate editcurves for strokes where the geometry/vertex colors have changed */ if (GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd)) { GP_EDITABLE_CURVES_BEGIN(gps_iter, C, gpl, gps, gpc) { if (gpc->flag & GP_CURVE_NEEDS_STROKE_UPDATE) { BKE_gpencil_stroke_editcurve_update(gpd, gpl, gps); /* Update the selection from the stroke to the curve. */ BKE_gpencil_editcurve_stroke_sync_selection(gpd, gps, gps->editcurve); gps->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; BKE_gpencil_stroke_geometry_update(gpd, gps); } } GP_EDITABLE_CURVES_END(gps_iter); } /* 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(bGPdata *gpd, 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; } } if ((start_idx != -1) || (start_idx == gps->totpoints - 1)) { 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, true); /* 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(gpd, 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); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); 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; } bool changed = false; if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { /* 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, true); BLI_strncpy( gpsd->runtime.tmp_layerinfo, gpl->info, sizeof(gpsd->runtime.tmp_layerinfo)); /* Initialize triangle information. */ BKE_gpencil_stroke_geometry_update(gpd, 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(gpd, 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; BKE_gpencil_stroke_select_index_reset(gps); changed = true; } } /* 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; } if (changed) { /* 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; } return OPERATOR_CANCELLED; } 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(bGPdata *gpd, 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 (ELEM(i, 0, 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, true); 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(gpd, gps); BKE_gpencil_stroke_geometry_update(gpd, gps_new); /* Deselect original 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) { BKE_gpencil_stroke_flip(gps); } /* Calc geometry data. */ BKE_gpencil_stroke_geometry_update(gpd, 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; BKE_gpencil_stroke_select_index_reset(gps); } } static void gpencil_curve_extrude_points(bGPdata *gpd, bGPDframe *gpf, bGPDstroke *gps, bGPDcurve *gpc) { const int old_num_points = gpc->tot_curve_points; const bool first_select = gpc->curve_points[0].flag & GP_CURVE_POINT_SELECT; bool last_select = gpc->curve_points[old_num_points - 1].flag & GP_CURVE_POINT_SELECT; /* iterate over middle points */ for (int i = 1; i < gpc->tot_curve_points - 1; i++) { bGPDcurve_point *gpc_pt = &gpc->curve_points[i]; /* Create new stroke if selected point */ if (gpc_pt->flag & GP_CURVE_POINT_SELECT) { bGPDstroke *gps_new = BKE_gpencil_stroke_duplicate(gps, false, false); gps_new->points = NULL; gps_new->flag &= ~GP_STROKE_CYCLIC; gps_new->prev = gps_new->next = NULL; gps_new->editcurve = BKE_gpencil_stroke_editcurve_new(2); bGPDcurve *new_gpc = gps_new->editcurve; for (int j = 0; j < new_gpc->tot_curve_points; j++) { bGPDcurve_point *gpc_pt_new = &new_gpc->curve_points[j]; memcpy(gpc_pt_new, gpc_pt, sizeof(bGPDcurve_point)); gpc_pt_new->flag &= ~GP_CURVE_POINT_SELECT; BEZT_DESEL_ALL(&gpc_pt_new->bezt); } /* select last point */ bGPDcurve_point *gpc_pt_last = &new_gpc->curve_points[1]; gpc_pt_last->flag |= GP_CURVE_POINT_SELECT; BEZT_SEL_IDX(&gpc_pt_last->bezt, 1); gps_new->editcurve->flag |= GP_CURVE_SELECT; BLI_insertlinkafter(&gpf->strokes, gps, gps_new); gps_new->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; BKE_gpencil_stroke_geometry_update(gpd, gps_new); gpc_pt->flag &= ~GP_CURVE_POINT_SELECT; BEZT_DESEL_ALL(&gpc_pt->bezt); } } /* Edge-case for single curve point. */ if (gpc->tot_curve_points == 1) { last_select = false; } if (first_select || last_select) { int new_num_points = old_num_points; if (first_select) { new_num_points++; } if (last_select) { new_num_points++; } /* Grow the array */ gpc->tot_curve_points = new_num_points; gpc->curve_points = MEM_recallocN(gpc->curve_points, sizeof(bGPDcurve_point) * new_num_points); if (first_select) { /* shift points by one */ memmove( &gpc->curve_points[1], &gpc->curve_points[0], sizeof(bGPDcurve_point) * old_num_points); bGPDcurve_point *old_first = &gpc->curve_points[1]; old_first->flag &= ~GP_CURVE_POINT_SELECT; BEZT_DESEL_ALL(&old_first->bezt); } if (last_select) { bGPDcurve_point *old_last = &gpc->curve_points[gpc->tot_curve_points - 2]; bGPDcurve_point *new_last = &gpc->curve_points[gpc->tot_curve_points - 1]; memcpy(new_last, old_last, sizeof(bGPDcurve_point)); old_last->flag &= ~GP_CURVE_POINT_SELECT; BEZT_DESEL_ALL(&old_last->bezt); } gps->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; BKE_gpencil_stroke_geometry_update(gpd, gps); } } static int gpencil_extrude_exec(bContext *C, wmOperator *op) { Object *obact = CTX_data_active_object(C); bGPdata *gpd = (bGPdata *)obact->data; const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); 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; } 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; } 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 (is_curve_edit) { if (gps->editcurve == NULL) { continue; } bGPDcurve *gpc = gps->editcurve; if (gpc->flag & GP_CURVE_SELECT) { gpencil_curve_extrude_points(gpd, gpf, gps, gpc); } } else { if (gps->flag & GP_STROKE_SELECT) { gpencil_add_move_points(gpd, gpf, gps); } } changed = true; } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } } } CTX_DATA_END; if (changed) { /* 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. * \{ */ 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); } 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; } 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); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); 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(); if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { /* 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, true); /* 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(gpd, 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(gpd, 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; const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); 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, false); } 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; BKE_gpencil_stroke_select_index_reset(gps); } CTX_DATA_END; /* Ensure that all the necessary colors exist */ new_colors = gpencil_copybuf_validate_colormap(C); if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { /* 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 reuse active frame or add a new frame if one * doesn't exist already depending on REC button status. */ if (IS_AUTOKEY_ON(scene) || (gpl->actframe == NULL)) { gpf = BKE_gpencil_layer_frame_get(gpl, scene->r.cfra, GP_GETFRAME_ADD_NEW); } else { gpf = BKE_gpencil_layer_frame_get(gpl, scene->r.cfra, GP_GETFRAME_USE_PREV); } if (gpf) { /* Create new stroke */ bGPDstroke *new_stroke = BKE_gpencil_stroke_duplicate(gps, true, true); new_stroke->runtime.tmp_layerinfo[0] = '\0'; new_stroke->next = new_stroke->prev = NULL; /* Calc geometry data. */ BKE_gpencil_stroke_geometry_update(gpd, 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; 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); const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd); /* 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. */ PropertyRNA *prop; char name[128]; prop = RNA_struct_find_property(op->ptr, "new_layer_name"); if (RNA_property_is_set(op->ptr, prop)) { RNA_property_string_get(op->ptr, prop, name); } else { strcpy(name, "GP_Layer"); } target_layer = BKE_gpencil_layer_addnew(gpd, name, true, false); } 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. */ CTX_DATA_BEGIN (C, bGPDlayer *, gpl_src, editable_gpencil_layers) { /* Skip if this is the layer we're moving strokes to. */ if (gpl_src == target_layer) { continue; } bGPDframe *init_gpf = (is_multiedit) ? gpl_src->frames.first : gpl_src->actframe; for (bGPDframe *gpf_src = init_gpf; gpf_src; gpf_src = gpf_src->next) { if ((gpf_src == gpl_src->actframe) || ((gpf_src->flag & GP_FRAME_SELECT) && (is_multiedit))) { if (gpf_src == NULL) { continue; } bGPDstroke *gpsn = NULL; BLI_listbase_clear(&strokes); for (bGPDstroke *gps = gpf_src->strokes.first; gps; gps = gpsn) { gpsn = gps->next; /* Skip strokes that are invalid for current view. */ if (ED_gpencil_stroke_can_use(C, gps) == false) { continue; } /* Check if the color is editable. */ if (ED_gpencil_stroke_material_editable(ob, gpl_src, gps) == false) { continue; } if (gps->flag & GP_STROKE_SELECT) { BLI_remlink(&gpf_src->strokes, gps); BLI_addtail(&strokes, gps); } } /* Paste them all in one go. */ if (strokes.first) { bGPDframe *gpf_dst = BKE_gpencil_layer_frame_get( target_layer, gpf_src->framenum, GP_GETFRAME_ADD_NEW); BLI_movelisttolist(&gpf_dst->strokes, &strokes); BLI_assert((strokes.first == strokes.last) && (strokes.first == NULL)); } } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } /* If new layer and autolock, lock old layer. */ if ((layer_num == -1) && (use_autolock)) { gpl_src->flag |= GP_LAYER_LOCKED; } } CTX_DATA_END; /* 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; } static void layer_new_name_get(bGPdata *gpd, char *rname) { int index = 0; LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { if (strstr(gpl->info, "GP_Layer")) { index++; } } if (index == 0) { BLI_strncpy(rname, "GP_Layer", 128); return; } char *name = BLI_sprintfN("%.*s.%03d", 128, "GP_Layer", index); BLI_strncpy(rname, name, 128); MEM_freeN(name); } static int gpencil_move_to_layer_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event)) { Object *ob = CTX_data_active_object(C); PropertyRNA *prop; if (RNA_int_get(op->ptr, "layer") == -1) { prop = RNA_struct_find_property(op->ptr, "new_layer_name"); if (!RNA_property_is_set(op->ptr, prop)) { char name[MAX_NAME]; bGPdata *gpd = ob->data; layer_new_name_get(gpd, name); RNA_property_string_set(op->ptr, prop, name); return WM_operator_props_dialog_popup(C, op, 200); } } return gpencil_move_to_layer_exec(C, op); } void GPENCIL_OT_move_to_layer(wmOperatorType *ot) { PropertyRNA *prop; /* 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->invoke = gpencil_move_to_layer_invoke; ot->poll = gpencil_stroke_edit_poll; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; /* GPencil layer to use. */ prop = RNA_def_int(ot->srna, "layer", 0, -1, INT_MAX, "Grease Pencil Layer", "", -1, INT_MAX); RNA_def_property_flag(prop, PROP_HIDDEN | PROP_SKIP_SAVE); prop = RNA_def_string( ot->srna, "new_layer_name", NULL, MAX_NAME, "Name", "Name of the newly added layer"); RNA_def_property_flag(prop, PROP_SKIP_SAVE); ot->prop = prop; } /** \} */ /* -------------------------------------------------------------------- */ /** \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 = scene->r.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, 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, scene->r.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, scene->r.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) { 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; } /* free stroke if selected */ if (gps->flag & GP_STROKE_SELECT) { BLI_remlink(&gpf->strokes, gps); /* free stroke memory arrays, then stroke itself */ BKE_gpencil_free_stroke(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; } /* ----------------------------------- */ static bool gpencil_dissolve_selected_curve_points(bContext *C, bGPdata *gpd, eGP_DissolveMode mode) { bool changed = false; GP_EDITABLE_CURVES_BEGIN(gps_iter, C, gpl, gps, gpc) { if (gpc->flag & GP_CURVE_SELECT) { int first = 0, last = 0; int num_points_remaining = gpc->tot_curve_points; switch (mode) { case GP_DISSOLVE_POINTS: for (int i = 0; i < gpc->tot_curve_points; i++) { bGPDcurve_point *cpt = &gpc->curve_points[i]; if (cpt->flag & GP_CURVE_POINT_SELECT) { num_points_remaining--; } } break; case GP_DISSOLVE_BETWEEN: first = -1; for (int i = 0; i < gpc->tot_curve_points; i++) { bGPDcurve_point *cpt = &gpc->curve_points[i]; if (cpt->flag & GP_CURVE_POINT_SELECT) { if (first < 0) { first = i; } last = i; } } for (int i = first + 1; i < last; i++) { bGPDcurve_point *cpt = &gpc->curve_points[i]; if ((cpt->flag & GP_CURVE_POINT_SELECT) == 0) { num_points_remaining--; } } break; case GP_DISSOLVE_UNSELECT: for (int i = 0; i < gpc->tot_curve_points; i++) { bGPDcurve_point *cpt = &gpc->curve_points[i]; if ((cpt->flag & GP_CURVE_POINT_SELECT) == 0) { num_points_remaining--; } } break; default: return false; break; } if (num_points_remaining < 1) { /* Delete stroke */ BLI_remlink(&gpf_->strokes, gps); BKE_gpencil_free_stroke(gps); } else { bGPDcurve_point *new_points = MEM_callocN(sizeof(bGPDcurve_point) * num_points_remaining, __func__); int idx = 0; switch (mode) { case GP_DISSOLVE_POINTS: for (int i = 0; i < gpc->tot_curve_points; i++) { bGPDcurve_point *cpt = &gpc->curve_points[i]; bGPDcurve_point *new_cpt = &new_points[idx]; if ((cpt->flag & GP_CURVE_POINT_SELECT) == 0) { *new_cpt = *cpt; idx++; } } break; case GP_DISSOLVE_BETWEEN: for (int i = 0; i < first; i++) { bGPDcurve_point *cpt = &gpc->curve_points[i]; bGPDcurve_point *new_cpt = &new_points[idx]; *new_cpt = *cpt; idx++; } for (int i = first; i < last; i++) { bGPDcurve_point *cpt = &gpc->curve_points[i]; bGPDcurve_point *new_cpt = &new_points[idx]; if (cpt->flag & GP_CURVE_POINT_SELECT) { *new_cpt = *cpt; idx++; } } for (int i = last; i < gpc->tot_curve_points; i++) { bGPDcurve_point *cpt = &gpc->curve_points[i]; bGPDcurve_point *new_cpt = &new_points[idx]; *new_cpt = *cpt; idx++; } break; case GP_DISSOLVE_UNSELECT: for (int i = 0; i < gpc->tot_curve_points; i++) { bGPDcurve_point *cpt = &gpc->curve_points[i]; bGPDcurve_point *new_cpt = &new_points[idx]; if (cpt->flag & GP_CURVE_POINT_SELECT) { *new_cpt = *cpt; idx++; } } break; default: return false; break; } if (gpc->curve_points != NULL) { MEM_freeN(gpc->curve_points); } gpc->curve_points = new_points; gpc->tot_curve_points = num_points_remaining; BKE_gpencil_editcurve_recalculate_handles(gps); gps->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; BKE_gpencil_stroke_geometry_update(gpd, gps); } changed = true; } } GP_EDITABLE_CURVES_END(gps_iter); return changed; } static bool gpencil_dissolve_selected_stroke_points(bContext *C, bGPdata *gpd, eGP_DissolveMode mode) { 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 */ BLI_remlink(&gpf_->strokes, gps); BKE_gpencil_free_stroke(gps); } 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(gpd, gps); /* deselect the stroke, since none of its selected points will still be selected */ gps->flag &= ~GP_STROKE_SELECT; BKE_gpencil_stroke_select_index_reset(gps); for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) { pt->flag &= ~GP_SPOINT_SELECT; } } changed = true; } } GP_EDITABLE_STROKES_END(gpstroke_iter); return changed; } /* 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; const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); bool changed = false; if (is_curve_edit) { changed = gpencil_dissolve_selected_curve_points(C, gpd, mode); } else { changed = gpencil_dissolve_selected_stroke_points(C, gpd, mode); } 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; } /* ----------------------------------- */ /* 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_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); 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_material_editable(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; BKE_gpencil_stroke_select_index_reset(gps); if (is_curve_edit) { bGPDcurve *gpc = gps->editcurve; BKE_gpencil_curve_delete_tagged_points( gpd, gpf, gps, gps->next, gpc, GP_CURVE_POINT_SELECT); } else { /* delete unwanted points by splitting stroke into several smaller ones */ BKE_gpencil_stroke_delete_tagged_points( gpd, gpf, gps, gps->next, GP_SPOINT_SELECT, false, 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; } 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 grid-step 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); ARegion *region = CTX_wm_region(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, region, NULL); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); bool changed = false; 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_layer_transform_matrix_get(depsgraph, obact, gpl, diff_mat); LISTBASE_FOREACH (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_material_editable(obact, gpl, gps) == false) { continue; } if (is_curve_edit) { if (gps->editcurve == NULL) { continue; } float inv_diff_mat[4][4]; invert_m4_m4_safe(inv_diff_mat, diff_mat); bGPDcurve *gpc = gps->editcurve; for (int i = 0; i < gpc->tot_curve_points; i++) { bGPDcurve_point *gpc_pt = &gpc->curve_points[i]; BezTriple *bezt = &gpc_pt->bezt; if (gpc_pt->flag & GP_CURVE_POINT_SELECT) { float tmp0[3], tmp1[3], tmp2[3], offset[3]; mul_v3_m4v3(tmp0, diff_mat, bezt->vec[0]); mul_v3_m4v3(tmp1, diff_mat, bezt->vec[1]); mul_v3_m4v3(tmp2, diff_mat, bezt->vec[2]); /* calculate the offset vector */ offset[0] = gridf * floorf(0.5f + tmp1[0] / gridf) - tmp1[0]; offset[1] = gridf * floorf(0.5f + tmp1[1] / gridf) - tmp1[1]; offset[2] = gridf * floorf(0.5f + tmp1[2] / gridf) - tmp1[2]; /* shift bezTriple */ add_v3_v3(bezt->vec[0], offset); add_v3_v3(bezt->vec[1], offset); add_v3_v3(bezt->vec[2], offset); mul_v3_m4v3(tmp0, inv_diff_mat, bezt->vec[0]); mul_v3_m4v3(tmp1, inv_diff_mat, bezt->vec[1]); mul_v3_m4v3(tmp2, inv_diff_mat, bezt->vec[2]); copy_v3_v3(bezt->vec[0], tmp0); copy_v3_v3(bezt->vec[1], tmp1); copy_v3_v3(bezt->vec[2], tmp2); changed = true; } } if (changed) { BKE_gpencil_editcurve_recalculate_handles(gps); gps->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; BKE_gpencil_stroke_geometry_update(gpd, gps); } } else { /* TODO: if entire stroke is selected, offset entire stroke by same amount? */ for (int i = 0; i < gps->totpoints; i++) { bGPDspoint *pt = &gps->points[i]; /* 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_world_to_object_space_point(depsgraph, obact, gpl, pt); changed = true; } } } } } } if (changed) { 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); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); 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; bool changed = false; if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { 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_layer_transform_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_material_editable(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); } changed = true; } 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_world_to_object_space_point(depsgraph, obact, gpl, pt); changed = true; } } } } } } } if (changed) { 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 bool gpencil_stroke_points_centroid(Depsgraph *depsgraph, bContext *C, Object *obact, bGPdata *gpd, float r_centroid[3], float r_min[3], float r_max[3], size_t *count) { bool changed = false; /* 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_layer_transform_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_material_editable(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(r_centroid, fpt); minmax_v3v3_v3(r_min, r_max, fpt); (*count)++; } } changed = true; } } } return changed; } static int gpencil_snap_cursor_to_sel(bContext *C, wmOperator *op) { Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); Object *obact = CTX_data_active_object(C); bGPdata *gpd = ED_gpencil_data_get_active(C); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); Scene *scene = CTX_data_scene(C); float *cursor = scene->cursor.location; float centroid[3] = {0.0f}; float min[3], max[3]; size_t count = 0; INIT_MINMAX(min, max); bool changed = false; if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { changed = gpencil_stroke_points_centroid(depsgraph, C, obact, gpd, centroid, min, max, &count); } if (changed) { 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, NULL); } return OPERATOR_FINISHED; } void GPENCIL_OT_snap_cursor_to_selected(wmOperatorType *ot) { /* identifiers */ ot->name = "Snap Cursor to Selected Points"; ot->idname = "GPENCIL_OT_snap_cursor_to_selected"; ot->description = "Snap cursor to center of selected points"; /* callbacks */ ot->exec = 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); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); bGPDstroke *gps = NULL; /* sanity checks */ if (ELEM(NULL, gpd)) { return OPERATOR_CANCELLED; } bool changed = false; /* 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; } bool before = (bool)(gps->flag & GP_STROKE_CYCLIC); 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; } if (before != (gps->flag & GP_STROKE_CYCLIC)) { /* Create new geometry. */ if (is_curve_edit) { BKE_gpencil_editcurve_recalculate_handles(gps); gps->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; BKE_gpencil_stroke_geometry_update(gpd, gps); } else if ((gps->flag & GP_STROKE_CYCLIC) && geometry) { BKE_gpencil_stroke_close(gps); BKE_gpencil_stroke_geometry_update(gpd, gps); } changed = true; } } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } } } CTX_DATA_END; if (changed) { /* 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; } static bool gpencil_cyclical_set_curve_edit_poll_property(const bContext *C, wmOperator *UNUSED(op), const PropertyRNA *prop) { bGPdata *gpd = ED_gpencil_data_get_active(C); if (gpd != NULL && GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd)) { const char *prop_id = RNA_property_identifier(prop); /* Only show type in curve edit mode */ if (!STREQ(prop_id, "type")) { return false; } } return true; } 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; ot->poll_property = gpencil_cyclical_set_curve_edit_poll_property; /* 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"); const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd); /* sanity checks */ if (ELEM(NULL, gpd)) { return OPERATOR_CANCELLED; } bool changed = false; /* 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 (bGPDstroke *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; } short prev_first = gps->caps[0]; short prev_last = gps->caps[1]; if (ELEM(type, GP_STROKE_CAPS_TOGGLE_BOTH, GP_STROKE_CAPS_TOGGLE_START)) { ++gps->caps[0]; if (gps->caps[0] >= GP_STROKE_CAP_MAX) { gps->caps[0] = GP_STROKE_CAP_ROUND; } } if (ELEM(type, GP_STROKE_CAPS_TOGGLE_BOTH, 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; } if (prev_first != gps->caps[0] || prev_last != gps->caps[1]) { changed = true; } } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } } } CTX_DATA_END; if (changed) { /* 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_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, "DEFAULT", 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 * \{ */ typedef struct tJoinStrokes { bGPDframe *gpf; bGPDstroke *gps; bool used; } tJoinStrokes; static int gpencil_get_nearest_stroke_index(tJoinStrokes *strokes_list, const bGPDstroke *gps, const int totstrokes) { int index = -1; float min_dist = FLT_MAX; float dist, start_a[3], end_a[3], start_b[3], end_b[3]; bGPDspoint *pt = &gps->points[0]; copy_v3_v3(start_a, &pt->x); pt = &gps->points[gps->totpoints - 1]; copy_v3_v3(end_a, &pt->x); for (int i = 0; i < totstrokes; i++) { tJoinStrokes *elem = &strokes_list[i]; if (elem->used) { continue; } pt = &elem->gps->points[0]; copy_v3_v3(start_b, &pt->x); pt = &elem->gps->points[elem->gps->totpoints - 1]; copy_v3_v3(end_b, &pt->x); dist = len_squared_v3v3(start_a, start_b); if (dist < min_dist) { min_dist = dist; index = i; } dist = len_squared_v3v3(start_a, end_b); if (dist < min_dist) { min_dist = dist; index = i; } dist = len_squared_v3v3(end_a, start_b); if (dist < min_dist) { min_dist = dist; index = i; } dist = len_squared_v3v3(end_a, end_b); if (dist < min_dist) { min_dist = dist; index = i; } } return index; } 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); /* Limit the number of strokes to join. It makes no sense to allow an very high number of * strokes for CPU time and because to have a stroke with thousands of points is unpractical, * so limit this number avoid to joining a full frame scene in one single stroke. */ const int max_join_strokes = 128; 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; } const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); if (is_curve_edit) { return OPERATOR_CANCELLED; } if (activegpl->flag & GP_LAYER_LOCKED) { return OPERATOR_CANCELLED; } BLI_assert(ELEM(type, GP_STROKE_JOIN, GP_STROKE_JOINCOPY)); int tot_strokes = 0; /** Alloc memory. */ tJoinStrokes *strokes_list = MEM_malloc_arrayN(max_join_strokes, sizeof(tJoinStrokes), __func__); tJoinStrokes *elem = NULL; /* Read all selected strokes to create a list. */ CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) { bGPDframe *gpf = gpl->actframe; if (gpf == NULL) { continue; } /* Add all stroke selected of the frame. */ 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_material_editable(ob, gpl, gps) == false) { continue; } elem = &strokes_list[tot_strokes]; elem->gpf = gpf; elem->gps = gps; elem->used = false; tot_strokes++; /* Limit the number of strokes. */ if (tot_strokes == max_join_strokes) { BKE_reportf(op->reports, RPT_WARNING, "Too many strokes selected, only joined first %d strokes", max_join_strokes); break; } } } } CTX_DATA_END; /* Nothing to join. */ if (tot_strokes < 2) { MEM_SAFE_FREE(strokes_list); return OPERATOR_CANCELLED; } /* Take first stroke. */ elem = &strokes_list[0]; elem->used = true; /* Create a new stroke. */ bGPDstroke *gps_new = BKE_gpencil_stroke_duplicate(elem->gps, true, true); gps_new->flag &= ~GP_STROKE_CYCLIC; BLI_insertlinkbefore(&elem->gpf->strokes, elem->gps, gps_new); /* Join all strokes until the list is completed. */ while (true) { int i = gpencil_get_nearest_stroke_index(strokes_list, gps_new, tot_strokes); if (i < 0) { break; } elem = &strokes_list[i]; /* Join new_stroke and stroke B. */ BKE_gpencil_stroke_join(gps_new, elem->gps, leave_gaps, true, false, true); elem->used = true; } /* Calc geometry data for new stroke. */ BKE_gpencil_stroke_geometry_update(gpd, gps_new); /* If join only, delete old strokes. */ if (type == GP_STROKE_JOIN) { for (int i = 0; i < tot_strokes; i++) { elem = &strokes_list[i]; BLI_remlink(&elem->gpf->strokes, elem->gps); BKE_gpencil_free_stroke(elem->gps); } } /* Free memory. */ MEM_SAFE_FREE(strokes_list); /* 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 *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; } const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); bool changed = false; /* Read 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; } 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_material_editable(ob, gpl, gps) == false) { continue; } if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { /* Flip stroke. */ BKE_gpencil_stroke_flip(gps); changed = true; } } } } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } } CTX_DATA_END; if (changed) { /* 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 Start Set Operator * \{ */ static int gpencil_stroke_start_set_exec(bContext *C, wmOperator *op) { Object *ob = CTX_data_active_object(C); bGPdata *gpd = ob->data; /* sanity checks */ if (ELEM(NULL, ob, gpd)) { return OPERATOR_CANCELLED; } const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Curve Edit mode not supported"); return OPERATOR_CANCELLED; } bool changed = false; /* Read 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; } 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_material_editable(ob, gpl, gps) == false) { continue; } /* Only cyclic strokes. */ if ((gps->flag & GP_STROKE_CYCLIC) == 0) { continue; } /* Find first selected point and set start. */ bGPDspoint *pt; for (int i = 0; i < gps->totpoints; i++) { pt = &gps->points[i]; if (pt->flag & GP_SPOINT_SELECT) { if (i == gps->totpoints - 1) { BKE_gpencil_stroke_flip(gps); } else { BKE_gpencil_stroke_start_set(gps, i); } BKE_gpencil_stroke_geometry_update(gpd, gps); changed = true; break; } } } } } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } } CTX_DATA_END; if (changed) { /* 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_start_set(wmOperatorType *ot) { /* identifiers */ ot->name = "Set Start Point"; ot->idname = "GPENCIL_OT_stroke_start_set"; ot->description = "Set start point for cyclic strokes"; /* api callbacks */ ot->exec = gpencil_stroke_start_set_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); 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"); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd); const float offset = RNA_float_get(op->ptr, "offset"); /* Init snap context for geometry projection. */ SnapObjectContext *sctx = NULL; sctx = ED_transform_snap_object_context_create(scene, 0); bool changed = false; /* Init space conversion stuff. */ GP_SpaceConversion gsc = {NULL}; gpencil_point_conversion_init(C, &gsc); int cfra_prv = INT_MIN; /* Go through each editable + selected stroke, adjusting each of its points one by one... */ 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 (bGPDstroke *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; } bool curve_select = false; if (is_curve_edit && gps->editcurve != NULL) { curve_select = gps->editcurve->flag & GP_CURVE_SELECT; } if (gps->flag & GP_STROKE_SELECT || curve_select) { /* update frame to get the new location of objects */ if ((mode == GP_REPROJECT_SURFACE) && (cfra_prv != gpf->framenum)) { cfra_prv = gpf->framenum; scene->r.cfra = gpf->framenum; BKE_scene_graph_update_for_newframe(depsgraph); } ED_gpencil_stroke_reproject( depsgraph, &gsc, sctx, gpl, gpf, gps, mode, keep_original, offset); if (is_curve_edit && gps->editcurve != NULL) { BKE_gpencil_stroke_editcurve_update(gpd, gpl, gps); /* Update the selection from the stroke to the curve. */ BKE_gpencil_editcurve_stroke_sync_selection(gpd, gps, gps->editcurve); gps->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; BKE_gpencil_stroke_geometry_update(gpd, gps); } changed = true; } } } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } } CTX_DATA_END; /* return frame state and DB to original state */ scene->r.cfra = oldframe; BKE_scene_graph_update_for_newframe(depsgraph); if (sctx != NULL) { ED_transform_snap_object_context_destroy(sctx); } if (changed) { /* 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; } static void gpencil_strokes_reproject_ui(bContext *UNUSED(C), wmOperator *op) { uiLayout *layout = op->layout; uiLayout *row; const eGP_ReprojectModes type = RNA_enum_get(op->ptr, "type"); uiLayoutSetPropSep(layout, true); uiLayoutSetPropDecorate(layout, false); row = uiLayoutRow(layout, true); uiItemR(row, op->ptr, "type", 0, NULL, ICON_NONE); if (type == GP_REPROJECT_SURFACE) { row = uiLayoutRow(layout, true); uiItemR(row, op->ptr, "offset", 0, NULL, ICON_NONE); } row = uiLayoutRow(layout, true); uiItemR(row, op->ptr, "keep_original", 0, NULL, ICON_NONE); } void GPENCIL_OT_reproject(wmOperatorType *ot) { PropertyRNA *prop; 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; ot->ui = gpencil_strokes_reproject_ui; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; /* properties */ ot->prop = RNA_def_enum( ot->srna, "type", reproject_type, GP_REPROJECT_VIEW, "Projection Type", ""); prop = RNA_def_boolean( ot->srna, "keep_original", 0, "Keep Original", "Keep original strokes and create a copy before reprojecting instead of reproject them"); RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_MOVIECLIP); RNA_def_float(ot->srna, "offset", 0.0f, 0.0f, 10.0f, "Surface Offset", "", 0.0f, 10.0f); } 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(gpd, 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; } /* -------------------------------------------------------------------- */ /** \name Stroke Perimeter from View Operator * \{ */ enum { GP_PERIMETER_VIEW = 0, GP_PERIMETER_FRONT = 1, GP_PERIMETER_SIDE = 2, GP_PERIMETER_TOP = 3, GP_PERIMETER_CAMERA = 4, }; enum { GP_STROKE_USE_ACTIVE_MATERIAL = 0, GP_STROKE_USE_CURRENT_MATERIAL = 1, GP_STROKE_USE_NEW_MATERIAL = 2, }; static int gpencil_stroke_outline_exec(bContext *C, wmOperator *op) { Main *bmain = CTX_data_main(C); RegionView3D *rv3d = CTX_wm_region_view3d(C); Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); Object *ob = CTX_data_active_object(C); bGPdata *gpd = (bGPdata *)ob->data; const int subdivisions = RNA_int_get(op->ptr, "subdivisions"); const float length = RNA_float_get(op->ptr, "length"); const bool keep = RNA_boolean_get(op->ptr, "keep"); const int thickness = RNA_int_get(op->ptr, "thickness"); const int view_mode = RNA_enum_get(op->ptr, "view_mode"); const int material_mode = RNA_enum_get(op->ptr, "material_mode"); const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd); /* sanity checks */ if (ELEM(NULL, gpd)) { return OPERATOR_CANCELLED; } bool changed = false; float viewmat[4][4]; copy_m4_m4(viewmat, rv3d->viewmat); switch (view_mode) { case GP_PERIMETER_FRONT: unit_m4(rv3d->viewmat); viewmat[1][1] = 0.0f; viewmat[1][2] = -1.0f; viewmat[2][1] = 1.0f; viewmat[2][2] = 0.0f; viewmat[3][2] = -10.0f; break; case GP_PERIMETER_SIDE: zero_m4(viewmat); viewmat[0][2] = 1.0f; viewmat[1][0] = 1.0f; viewmat[2][1] = 1.0f; viewmat[3][3] = 1.0f; break; case GP_PERIMETER_TOP: unit_m4(viewmat); break; case GP_PERIMETER_CAMERA: { Scene *scene = CTX_data_scene(C); Object *cam_ob = scene->camera; if (cam_ob != NULL) { invert_m4_m4(viewmat, cam_ob->object_to_world); } break; } default: break; } /* Untag strokes to be sure nothing is pending. */ LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) { LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) { gps->flag &= ~GP_STROKE_TAG; } } } /* Create a new material. */ int mat_idx = 0; if (material_mode == GP_STROKE_USE_NEW_MATERIAL) { Material *ma = BKE_gpencil_object_material_new(bmain, ob, "Material", NULL); MaterialGPencilStyle *gp_style = ma->gp_style; gp_style->flag |= GP_MATERIAL_FILL_SHOW; mat_idx = ob->totcol - 1; } /* loop all selected strokes */ LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { if (gpl->flag & GP_LAYER_HIDE) { continue; } /* Prepare transform matrix. */ float diff_mat[4][4]; BKE_gpencil_layer_transform_matrix_get(depsgraph, ob, gpl, diff_mat); 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 (bGPDstroke *gps = gpf->strokes.first; gps; gps = gps->next) { if ((gps->flag & GP_STROKE_SELECT) == 0) { continue; } if (gps->totpoints == 0) { continue; } if (!ED_gpencil_stroke_material_visible(ob, gps)) { continue; } MaterialGPencilStyle *gp_style = BKE_gpencil_material_settings(ob, gps->mat_nr + 1); const bool is_stroke = ((gp_style->flag & GP_MATERIAL_STROKE_SHOW) != 0); if (!is_stroke) { continue; } /* Duplicate the stroke to apply any layer thickness change. */ bGPDstroke *gps_duplicate = BKE_gpencil_stroke_duplicate(gps, true, false); /* Apply layer thickness change. */ gps_duplicate->thickness += gpl->line_change; /* Apply object scale to thickness. */ gps_duplicate->thickness *= mat4_to_scale(ob->object_to_world); CLAMP_MIN(gps_duplicate->thickness, 1.0f); /* Stroke. */ const float ovr_thickness = keep ? thickness : 0.0f; bGPDstroke *gps_perimeter = BKE_gpencil_stroke_perimeter_from_view( viewmat, gpd, gpl, gps_duplicate, subdivisions, diff_mat, ovr_thickness); gps_perimeter->flag &= ~GP_STROKE_SELECT; /* Assign material. */ switch (material_mode) { case GP_STROKE_USE_ACTIVE_MATERIAL: { if (ob->actcol - 1 < 0) { gps_perimeter->mat_nr = 0; } else { gps_perimeter->mat_nr = ob->actcol - 1; } break; } case GP_STROKE_USE_CURRENT_MATERIAL: gps_perimeter->mat_nr = gps->mat_nr; break; case GP_STROKE_USE_NEW_MATERIAL: gps_perimeter->mat_nr = mat_idx; break; default: break; } /* Sample stroke. */ if (length > 0.0f) { BKE_gpencil_stroke_sample(gpd, gps_perimeter, length, false, 0); } /* Set stroke thickness. */ gps_perimeter->thickness = thickness; /* Set pressure constant. */ bGPDspoint *pt; for (int i = 0; i < gps_perimeter->totpoints; i++) { pt = &gps_perimeter->points[i]; pt->pressure = 1.0f; } /* Add perimeter stroke to frame. */ BLI_insertlinkafter(&gpf->strokes, gps, gps_perimeter); /* Tag original stroke to be removed. */ gps->flag |= GP_STROKE_TAG; /* Free Temp stroke. */ BKE_gpencil_free_stroke(gps_duplicate); changed = true; } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } } } /* Free old strokes. */ LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) { LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) { if (gps->flag & GP_STROKE_TAG) { BLI_remlink(&gpf->strokes, gps); BKE_gpencil_free_stroke(gps); } } } } if (changed) { /* 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_outline(wmOperatorType *ot) { static const EnumPropertyItem view_mode[] = { {GP_PERIMETER_VIEW, "VIEW", 0, "View", ""}, {GP_PERIMETER_FRONT, "FRONT", 0, "Front", ""}, {GP_PERIMETER_SIDE, "SIDE", 0, "Side", ""}, {GP_PERIMETER_TOP, "TOP", 0, "Top", ""}, {GP_PERIMETER_CAMERA, "CAMERA", 0, "Camera", ""}, {0, NULL, 0, NULL, NULL}, }; static const EnumPropertyItem material_mode[] = { {GP_STROKE_USE_ACTIVE_MATERIAL, "ACTIVE", 0, "Active Material", ""}, {GP_STROKE_USE_CURRENT_MATERIAL, "KEEP", 0, "Keep Material", "Keep current stroke material"}, {GP_STROKE_USE_NEW_MATERIAL, "NEW", 0, "New Material", ""}, {0, NULL, 0, NULL, NULL}, }; /* identifiers */ ot->name = "Convert Stroke to Outline"; ot->idname = "GPENCIL_OT_stroke_outline"; ot->description = "Convert stroke to perimeter"; /* api callbacks */ ot->exec = gpencil_stroke_outline_exec; ot->poll = gpencil_stroke_edit_poll; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; /* properties */ ot->prop = RNA_def_enum(ot->srna, "view_mode", view_mode, GP_PERIMETER_VIEW, "View", ""); RNA_def_enum(ot->srna, "material_mode", material_mode, GP_STROKE_USE_ACTIVE_MATERIAL, "Material Mode", ""); RNA_def_int(ot->srna, "thickness", 1, 1, 1000, "Thickness", "Thickness of the stroke perimeter", 1, 1000); RNA_def_boolean(ot->srna, "keep", true, "Keep Shape", "Try to keep global shape when the stroke thickness change"); RNA_def_int(ot->srna, "subdivisions", 3, 0, 10, "Subdivisions", "", 0, 10); RNA_def_float(ot->srna, "length", 0.0f, 0.0f, 100.0f, "Sample Length", "", 0.0f, 100.0f); } /** \} */ 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) { /* TODO use `BKE_gpencil_stroke_smooth` when the weights are better used. */ bGPDstroke gps_old = *gps; gps_old.points = (bGPDspoint *)MEM_dupallocN(gps->points); bool need_update = false; /* Here the iteration needs to be done outside the smooth functions, * as there are points that don't get smoothed. */ for (int n = 0; n < repeat; n++) { for (int i = 0; i < gps->totpoints; i++) { if (only_selected && (gps->points[i].flag & GP_SPOINT_SELECT) == 0) { continue; } /* Perform smoothing. */ if (smooth_position) { BKE_gpencil_stroke_smooth_point(&gps_old, i, factor, 1, false, false, gps); need_update = true; } if (smooth_strength) { BKE_gpencil_stroke_smooth_strength(&gps_old, i, factor, 1, gps); } if (smooth_thickness) { BKE_gpencil_stroke_smooth_thickness(&gps_old, i, 1.0f - factor, 1, gps); } if (smooth_uv) { BKE_gpencil_stroke_smooth_uv(&gps_old, i, factor, 1, gps); need_update = true; } } if (n < repeat - 1) { memcpy(gps_old.points, gps->points, sizeof(bGPDspoint) * gps->totpoints); } } MEM_freeN(gps_old.points); if (need_update) { gps->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; BKE_gpencil_stroke_geometry_update(gpd_, gps); } } } 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++; } } } } if ((gps->flag & GP_STROKE_CYCLIC) && (gps->points[0].flag & GP_SPOINT_SELECT) && (gps->points[gps->totpoints - 1].flag & GP_SPOINT_SELECT)) { totnewpoints++; } return totnewpoints; } static void gpencil_stroke_subdivide(bGPDstroke *gps, const int cuts) { bGPDspoint *temp_points; int totnewpoints, oldtotpoints; int i2; /* 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++; } } } } /* Subdivide between last and first point. */ if (gps->flag & GP_STROKE_CYCLIC) { bGPDspoint *pt = &temp_points[oldtotpoints - 1]; bGPDspoint *next = &temp_points[0]; if ((pt->flag & GP_SPOINT_SELECT) && (next->flag & GP_SPOINT_SELECT)) { bGPDspoint *pt_final = &gps->points[i2]; if (gps->dvert != NULL) { dvert_final = &gps->dvert[i2]; } /* Interpolate all values */ 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[oldtotpoints - 1]; dvert_next = &temp_dverts[0]; 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); } } } } } /* free temp memory */ MEM_SAFE_FREE(temp_points); MEM_SAFE_FREE(temp_dverts); } } static int gpencil_stroke_subdivide_exec(bContext *C, wmOperator *op) { bGPdata *gpd = ED_gpencil_data_get_active(C); const int cuts = RNA_int_get(op->ptr, "number_cuts"); /* sanity checks */ if (ELEM(NULL, gpd)) { return OPERATOR_CANCELLED; } const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); bool changed = false; if (is_curve_edit) { GP_EDITABLE_CURVES_BEGIN(gps_iter, C, gpl, gps, gpc) { if (gpc->flag & GP_CURVE_SELECT) { BKE_gpencil_editcurve_subdivide(gps, cuts); BKE_gpencil_editcurve_recalculate_handles(gps); gps->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; BKE_gpencil_stroke_geometry_update(gpd, gps); changed = true; } } GP_EDITABLE_CURVES_END(gps_iter); } else { /* Go through each editable + selected stroke */ GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) { if (gps->flag & GP_STROKE_SELECT) { gpencil_stroke_subdivide(gps, cuts); /* Calc geometry data. */ BKE_gpencil_stroke_geometry_update(gpd, gps); changed = true; } } GP_EDITABLE_STROKES_END(gpstroke_iter); if (changed) { /* smooth stroke */ gpencil_smooth_stroke(C, op); } } if (changed) { /* 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; } static bool gpencil_subdivide_curve_edit_poll_property(const bContext *C, wmOperator *UNUSED(op), const PropertyRNA *prop) { bGPdata *gpd = ED_gpencil_data_get_active(C); if (gpd != NULL && GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd)) { const char *prop_id = RNA_property_identifier(prop); /* Only show number_cuts in curve edit mode */ if (!STREQ(prop_id, "number_cuts")) { return false; } } return true; } 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; ot->poll_property = gpencil_subdivide_curve_edit_poll_property; /* 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; } const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); bool changed = false; if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { /* 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(gpd, gps, factor); changed = true; } } GP_EDITABLE_STROKES_END(gpstroke_iter); } if (changed) { /* 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; } const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); bool changed = false; if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { /* Go through each editable + selected stroke */ GP_EDITABLE_STROKES_BEGIN (gpstroke_iter, C, gpl, gps) { if (gps->flag & GP_STROKE_SELECT) { changed |= true; for (int i = 0; i < steps; i++) { BKE_gpencil_stroke_simplify_fixed(gpd, gps); } } } GP_EDITABLE_STROKES_END(gpstroke_iter); } if (changed) { /* 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"); const float sharp_threshold = RNA_float_get(op->ptr, "sharp_threshold"); /* 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(gpd, gps, length, true, sharp_threshold); } } 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_stroke_not_in_curve_edit_mode; /* 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); prop = RNA_def_float( ot->srna, "sharp_threshold", 0.1f, 0.0f, M_PI, "Sharp Threshold", "", 0.0f, M_PI); /* 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 *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); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_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) { if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { BKE_gpencil_stroke_trim(gpd, gps); } } } /* If not multi-edit, 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"); const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd_src); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd_src); /* sanity checks */ if (ELEM(NULL, gpd_src)) { return OPERATOR_CANCELLED; } if ((mode == GP_SEPARATE_LAYER) && BLI_listbase_is_single(&gpd_src->layers)) { BKE_report(op->reports, RPT_ERROR, "Cannot separate an object with one layer only"); return OPERATOR_CANCELLED; } /* Cancel if nothing selected. */ if (ELEM(mode, GP_SEPARATE_POINT, GP_SEPARATE_STROKE)) { bool has_selected = false; CTX_DATA_BEGIN (C, bGPDlayer *, gpl, editable_gpencil_layers) { if (ED_gpencil_layer_has_selected_stroke(gpl, is_multiedit)) { has_selected = true; break; } } CTX_DATA_END; if (!has_selected) { BKE_report(op->reports, RPT_ERROR, "Nothing selected"); return OPERATOR_CANCELLED; } } /* 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; BKE_defgroup_copy_list(&gpd_dst->vertex_group_names, &gpd_src->vertex_group_names); gpd_dst->vertex_group_active_index = gpd_src->vertex_group_active_index; /* Loop old data-block and separate parts. */ if (ELEM(mode, GP_SEPARATE_POINT, 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_material_editable(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, false); BKE_gpencil_layer_copy_settings(gpl, gpl_dst); /* Copy masks. */ BKE_gpencil_layer_mask_copy(gpl, gpl_dst); } /* 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) { if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { /* Check if all points are selected. */ bool all_points_selected = true; for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) { if ((pt->flag & GP_SPOINT_SELECT) == 0) { all_points_selected = false; break; } } /* Separate the entire stroke. */ if (all_points_selected) { /* deselect old stroke */ gps->flag &= ~GP_STROKE_SELECT; BKE_gpencil_stroke_select_index_reset(gps); /* 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; continue; } /* make copy of source stroke */ bGPDstroke *gps_dst = BKE_gpencil_stroke_duplicate(gps, true, 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 */ BKE_gpencil_stroke_delete_tagged_points( gpd_dst, gpf_dst, gps_dst, NULL, GP_SPOINT_SELECT, false, false, 0); /* delete selected points from origin stroke */ BKE_gpencil_stroke_delete_tagged_points( gpd_src, gpf, gps, gps->next, GP_SPOINT_SELECT, false, false, 0); } } /* selected strokes mode */ else if (mode == GP_SEPARATE_STROKE) { /* deselect old stroke */ gps->flag &= ~GP_STROKE_SELECT; BKE_gpencil_stroke_select_index_reset(gps); /* 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 multi-edit, 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, slot)) { ob_dst->actcol = slot; if (!BKE_object_material_slot_remove(bmain, ob_dst)) { break; } if (actcol >= slot) { actcol--; } } } ob_dst->actcol = actcol; /* Remove any invalid Mask relationship. */ BKE_gpencil_layer_mask_cleanup_all_layers(gpd_dst); 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 *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); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_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_material_editable(ob, gpl, gps) == false) { continue; } /* Split selected strokes. */ if (gps->flag & GP_STROKE_SELECT) { if (is_curve_edit) { BKE_report(op->reports, RPT_ERROR, "Not implemented!"); } else { /* make copy of source stroke */ bGPDstroke *gps_dst = BKE_gpencil_stroke_duplicate(gps, true, 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 */ BKE_gpencil_stroke_delete_tagged_points( gpd, gpf, gps_dst, NULL, GP_SPOINT_SELECT, true, false, 0); /* delete selected points from origin stroke */ BKE_gpencil_stroke_delete_tagged_points( gpd, gpf, gps, gps->next, GP_SPOINT_SELECT, false, 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 multi-edit, 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_stroke_not_in_curve_edit_mode; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; /* properties */ prop = RNA_def_int(ot->srna, "repeat", 2, 1, 1000, "Repeat", "", 1, 1000); RNA_def_property_flag(prop, PROP_SKIP_SAVE); RNA_def_float(ot->srna, "factor", 1.0f, 0.0f, 2.0f, "Factor", "", 0.0f, 1.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_world_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(bGPdata *gpd, bGPDlayer *hit_layer, bGPDstroke *hit_stroke, const bool flat_caps) { 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; } } /* If flat caps mode check extremes. */ if (flat_caps) { if (hit_stroke->points[0].flag & GP_SPOINT_TAG) { hit_stroke->caps[0] = GP_STROKE_CAP_FLAT; } if (hit_stroke->points[hit_stroke->totpoints - 1].flag & GP_SPOINT_TAG) { hit_stroke->caps[1] = GP_STROKE_CAP_FLAT; } } BKE_gpencil_stroke_delete_tagged_points( gpd, hit_layer->actframe, hit_stroke, gpsn, GP_SPOINT_TAG, false, flat_caps, 1); } } static int gpencil_cutter_lasso_select(bContext *C, wmOperator *op, GPencilTestFn is_inside_fn, void *user_data) { Depsgraph *depsgraph = CTX_data_ensure_evaluated_depsgraph(C); Object *obact = CTX_data_active_object(C); 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; const bool flat_caps = RNA_boolean_get(op->ptr, "flat_caps"); const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd); 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. */ LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { bGPDframe *init_gpf = (is_multiedit) ? gpl->frames.first : gpl->actframe; for (bGPDframe *gpf = init_gpf; gpf; gpf = gpf->next) { LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) { if (gps->flag & GP_STROKE_SELECT) { int i; for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) { pt->flag &= ~GP_SPOINT_SELECT; } gps->flag &= ~GP_STROKE_SELECT; BKE_gpencil_stroke_select_index_reset(gps); } } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } } /* Select points */ LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { if ((gpl->flag & GP_LAYER_LOCKED) || (gpl->flag & GP_LAYER_HIDE)) { continue; } float diff_mat[4][4]; BKE_gpencil_layer_transform_matrix_get(depsgraph, obact, gpl, diff_mat); 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 (bGPDstroke *, gps, &gpf->strokes) { if (ED_gpencil_stroke_can_use(C, gps) == false) { continue; } /* check if the color is editable */ if (ED_gpencil_stroke_material_editable(obact, gpl, gps) == false) { continue; } 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, diff_mat, user_data); if (is_inside) { tot_inside++; changed = true; pt->flag |= GP_SPOINT_SELECT; gps->flag |= GP_STROKE_SELECT; BKE_gpencil_stroke_select_index_set(gpd, gps); float r_hita[3], r_hitb[3]; if (gps->totpoints > 1) { ED_gpencil_select_stroke_segment( gpd, 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; } } } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } } } /* Dissolve selected points. */ LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { bGPDframe *init_gpf = (is_multiedit) ? gpl->frames.first : gpl->actframe; bGPDframe *gpf_act = gpl->actframe; for (bGPDframe *gpf = init_gpf; gpf; gpf = gpf->next) { gpl->actframe = gpf; LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) { if (gps->flag & GP_STROKE_SELECT) { gpencil_cutter_dissolve(gpd, gpl, gps, flat_caps); } } /* If not multi-edit, exit loop. */ if (!is_multiedit) { break; } } gpl->actframe = gpf_act; } /* 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 | OPTYPE_DEPENDS_ON_CURSOR; /* properties */ WM_operator_properties_gesture_lasso(ot); RNA_def_boolean(ot->srna, "flat_caps", 0, "Flat Caps", ""); } 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; } const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_SESSIONS_ON(gpd); if (is_curve_edit) { /* TODO: merge curve points by distance */ } else { /* 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(gpd, 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); } /** \} */ /* -------------------------------------------------------------------- */ /** \name Stroke Normalize Operator * \{ */ typedef enum eGP_NormalizeMode { GP_NORMALIZE_THICKNESS = 0, GP_NORMALIZE_OPACITY, } eGP_NormalizeMode; static bool gpencil_stroke_normalize_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 void gpencil_stroke_normalize_ui(bContext *UNUSED(C), wmOperator *op) { uiLayout *layout = op->layout; uiLayout *row; const eGP_NormalizeMode mode = RNA_enum_get(op->ptr, "mode"); uiLayoutSetPropSep(layout, true); uiLayoutSetPropDecorate(layout, false); row = uiLayoutRow(layout, true); uiItemR(row, op->ptr, "mode", 0, NULL, ICON_NONE); if (mode == GP_NORMALIZE_THICKNESS) { row = uiLayoutRow(layout, true); uiItemR(row, op->ptr, "value", 0, NULL, ICON_NONE); } else if (mode == GP_NORMALIZE_OPACITY) { row = uiLayoutRow(layout, true); uiItemR(row, op->ptr, "factor", 0, NULL, ICON_NONE); } } static int gpencil_stroke_normalize_exec(bContext *C, wmOperator *op) { bGPdata *gpd = ED_gpencil_data_get_active(C); /* Sanity checks. */ if (ELEM(NULL, gpd)) { return OPERATOR_CANCELLED; } const eGP_NormalizeMode mode = RNA_enum_get(op->ptr, "mode"); const int value = RNA_int_get(op->ptr, "value"); const float factor = RNA_float_get(op->ptr, "factor"); /* Go through each editable + selected stroke. */ const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd); const bool is_curve_edit = (bool)GPENCIL_CURVE_EDIT_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 (bGPDstroke *, gps, &gpf->strokes) { /* Skip strokes that are invalid for current view. */ if (ED_gpencil_stroke_can_use(C, gps) == false) { continue; } bool is_curve_ready = (gps->editcurve != NULL); bool selected = (is_curve_edit && is_curve_ready) ? (gps->editcurve->flag & GP_CURVE_SELECT) : (gps->flag & GP_STROKE_SELECT); if (!selected) { continue; } float stroke_thickness_inv = 1.0f / max_ii(gps->thickness, 1); /* Fill opacity need to be managed before. */ if (mode == GP_NORMALIZE_OPACITY) { gps->fill_opacity_fac = factor; CLAMP(gps->fill_opacity_fac, 0.0f, 1.0f); } /* Loop all Polyline points. */ if (!is_curve_edit || !is_curve_ready) { for (int i = 0; i < gps->totpoints; i++) { bGPDspoint *pt = &gps->points[i]; if (mode == GP_NORMALIZE_THICKNESS) { pt->pressure = max_ff((float)value * stroke_thickness_inv, 0.0f); } else if (mode == GP_NORMALIZE_OPACITY) { pt->strength = factor; CLAMP(pt->strength, 0.0f, 1.0f); } } } else { /* Loop all Bezier points. */ for (int i = 0; i < gps->editcurve->tot_curve_points; i++) { bGPDcurve_point *gpc_pt = &gps->editcurve->curve_points[i]; if (mode == GP_NORMALIZE_THICKNESS) { gpc_pt->pressure = max_ff((float)value * stroke_thickness_inv, 0.0f); } else if (mode == GP_NORMALIZE_OPACITY) { gpc_pt->strength = factor; CLAMP(gpc_pt->strength, 0.0f, 1.0f); } } gps->flag |= GP_STROKE_NEEDS_CURVE_UPDATE; BKE_gpencil_stroke_geometry_update(gpd, gps); } } /* If not multi-edit, 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_normalize(wmOperatorType *ot) { static const EnumPropertyItem prop_gpencil_normalize_modes[] = { {GP_NORMALIZE_THICKNESS, "THICKNESS", 0, "Thickness", "Normalizes the stroke thickness by making all points use the same thickness value"}, {GP_NORMALIZE_OPACITY, "OPACITY", 0, "Opacity", "Normalizes the stroke opacity by making all points use the same opacity value"}, {0, NULL, 0, NULL, NULL}, }; /* identifiers */ ot->name = "Normalize Stroke"; ot->idname = "GPENCIL_OT_stroke_normalize"; ot->description = "Normalize stroke attributes"; /* api callbacks */ ot->exec = gpencil_stroke_normalize_exec; ot->poll = gpencil_stroke_normalize_poll; ot->ui = gpencil_stroke_normalize_ui; /* flags */ ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; /* props */ ot->prop = RNA_def_enum( ot->srna, "mode", prop_gpencil_normalize_modes, 0, "Mode", "Attribute to be normalized"); RNA_def_float(ot->srna, "factor", 1.0f, 0.0f, 1.0f, "Factor", "", 0.0f, 1.0f); RNA_def_int(ot->srna, "value", 10, 0, 1000, "Value", "Value", 0, 1000); } /** \} */