/* * ***** BEGIN GPL LICENSE BLOCK ***** * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * * The Original Code is Copyright (C) 2017, Blender Foundation * This is a new part of Blender * * Contributor(s): Antonio Vazquez * * ***** END GPL LICENSE BLOCK ***** * * Operators for creating new Grease Pencil primitives (boxes, circles, ...) */ /** \file blender/editors/gpencil/gpencil_primitive.c * \ingroup edgpencil */ #include #include #include #include #include #include "MEM_guardedalloc.h" #include "BLI_blenlib.h" #include "BLI_utildefines.h" #include "BLI_math.h" #include "BLT_translation.h" #include "DNA_brush_types.h" #include "DNA_gpencil_types.h" #include "DNA_meshdata_types.h" #include "DNA_object_types.h" #include "DNA_scene_types.h" #include "DNA_screen_types.h" #include "DNA_space_types.h" #include "DNA_view3d_types.h" #include "BKE_main.h" #include "BKE_brush.h" #include "BKE_context.h" #include "BKE_global.h" #include "BKE_gpencil.h" #include "BKE_library.h" #include "BKE_material.h" #include "BKE_paint.h" #include "BKE_report.h" #include "UI_interface.h" #include "WM_api.h" #include "WM_types.h" #include "RNA_access.h" #include "RNA_define.h" #include "RNA_enum_types.h" #include "ED_gpencil.h" #include "ED_object.h" #include "ED_screen.h" #include "ED_view3d.h" #include "ED_space_api.h" #include "DEG_depsgraph.h" #include "DEG_depsgraph_query.h" #include "gpencil_intern.h" #define MIN_EDGES 2 #define MAX_EDGES 100 #define IDLE 0 #define IN_PROGRESS 1 /* ************************************************ */ /* Core/Shared Utilities */ /* Poll callback for primitive operators */ static bool gpencil_primitive_add_poll(bContext *C) { /* only 3D view */ ScrArea *sa = CTX_wm_area(C); if (sa && sa->spacetype != SPACE_VIEW3D) { return 0; } /* need data to create primitive */ bGPdata *gpd = CTX_data_gpencil_data(C); if (gpd == NULL) { return 0; } /* only in edit and paint modes * - paint as it's the "drawing/creation mode" * - edit as this is more of an atomic editing operation * (similar to copy/paste), and also for consistency */ if ((gpd->flag & (GP_DATA_STROKE_PAINTMODE | GP_DATA_STROKE_EDITMODE)) == 0) { CTX_wm_operator_poll_msg_set(C, "Primitives can only be added in Draw or Edit modes"); return 0; } /* don't allow operator to function if the active layer is locked/hidden * (BUT, if there isn't an active layer, we are free to add new layer when the time comes) */ bGPDlayer *gpl = BKE_gpencil_layer_getactive(gpd); if ((gpl) && (gpl->flag & (GP_LAYER_LOCKED | GP_LAYER_HIDE))) { CTX_wm_operator_poll_msg_set(C, "Primitives cannot be added as active layer is locked or hidden"); return 0; } return 1; } /* ****************** Primitive Interactive *********************** */ /* Helper: Create internal strokes primitives data */ static void gp_primitive_set_initdata(bContext *C, tGPDprimitive *tgpi) { ToolSettings *ts = CTX_data_tool_settings(C); Depsgraph *depsgraph = CTX_data_depsgraph(C); int cfra_eval = (int)DEG_get_ctime(depsgraph); bGPDlayer *gpl = CTX_data_active_gpencil_layer(C); Brush *brush; /* if brush doesn't exist, create a new one */ Paint *paint = BKE_brush_get_gpencil_paint(ts); /* if not exist, create a new one */ if (paint->brush == NULL) { /* create new brushes */ BKE_brush_gpencil_presets(C); brush = BKE_brush_getactive_gpencil(ts); } else { /* Use the current */ brush = BKE_brush_getactive_gpencil(ts); } tgpi->brush = brush; /* if layer doesn't exist, create a new one */ if (gpl == NULL) { gpl = BKE_gpencil_layer_addnew(tgpi->gpd, DATA_("Primitives"), true); } tgpi->gpl = gpl; /* create a new temporary frame */ tgpi->gpf = MEM_callocN(sizeof(bGPDframe), "Temp bGPDframe"); tgpi->gpf->framenum = tgpi->cframe = cfra_eval; /* create new temp stroke */ bGPDstroke *gps = MEM_callocN(sizeof(bGPDstroke), "Temp bGPDstroke"); gps->thickness = 2.0f; gps->inittime = 0.0f; /* enable recalculation flag by default */ gps->flag |= GP_STROKE_RECALC_CACHES; /* the polygon must be closed, so enabled cyclic */ gps->flag |= GP_STROKE_CYCLIC; gps->flag |= GP_STROKE_3DSPACE; gps->mat_nr = BKE_object_material_slot_find_index(tgpi->ob, tgpi->mat) - 1; /* allocate memory for storage points, but keep empty */ gps->totpoints = 0; gps->points = MEM_callocN(sizeof(bGPDspoint), "gp_stroke_points"); gps->dvert = MEM_callocN(sizeof(MDeformVert), "gp_stroke_weights"); /* initialize triangle memory to dummy data */ gps->tot_triangles = 0; gps->triangles = NULL; gps->flag |= GP_STROKE_RECALC_CACHES; /* add to strokes */ BLI_addtail(&tgpi->gpf->strokes, gps); } /* ----------------------- */ /* Drawing Callbacks */ /* Drawing callback for modal operator in 3d mode */ static void gpencil_primitive_draw_3d(const bContext *C, ARegion *UNUSED(ar), void *arg) { tGPDprimitive *tgpi = (tGPDprimitive *)arg; ED_gp_draw_primitives(C, tgpi, REGION_DRAW_POST_VIEW); } /* ----------------------- */ /* Helper: Draw status message while the user is running the operator */ static void gpencil_primitive_status_indicators(bContext *C, tGPDprimitive *tgpi) { Scene *scene = tgpi->scene; char status_str[UI_MAX_DRAW_STR]; char msg_str[UI_MAX_DRAW_STR]; if (tgpi->type == GP_STROKE_BOX) { BLI_strncpy(msg_str, IFACE_("Rectangle: ESC/RMB to cancel, LMB set origin, Enter/LMB to confirm, Shift to square"), UI_MAX_DRAW_STR); } else if (tgpi->type == GP_STROKE_LINE) { BLI_strncpy(msg_str, IFACE_("Line: ESC/RMB to cancel, LMB set origin, Enter/LMB to confirm"), UI_MAX_DRAW_STR); } else { BLI_strncpy(msg_str, IFACE_("Circle: ESC/RMB to cancel, Enter/LMB to confirm, WHEEL to adjust edge number, Shift to square"), UI_MAX_DRAW_STR); } if (tgpi->type == GP_STROKE_CIRCLE) { if (hasNumInput(&tgpi->num)) { char str_offs[NUM_STR_REP_LEN]; outputNumInput(&tgpi->num, str_offs, &scene->unit); BLI_snprintf(status_str, sizeof(status_str), "%s: %s", msg_str, str_offs); } else { if (tgpi->flag == IN_PROGRESS) { BLI_snprintf(status_str, sizeof(status_str), "%s: %d (%d, %d) (%d, %d)", msg_str, (int)tgpi->tot_edges, tgpi->top[0], tgpi->top[1], tgpi->bottom[0], tgpi->bottom[1]); } else { BLI_snprintf(status_str, sizeof(status_str), "%s: %d (%d, %d)", msg_str, (int)tgpi->tot_edges, tgpi->bottom[0], tgpi->bottom[1]); } } } else { if (tgpi->flag == IN_PROGRESS) { BLI_snprintf(status_str, sizeof(status_str), "%s: (%d, %d) (%d, %d)", msg_str, tgpi->top[0], tgpi->top[1], tgpi->bottom[0], tgpi->bottom[1]); } else { BLI_snprintf(status_str, sizeof(status_str), "%s: (%d, %d)", msg_str, tgpi->bottom[0], tgpi->bottom[1]); } } ED_workspace_status_text(C, status_str); } /* ----------------------- */ /* create a rectangle */ static void gp_primitive_rectangle(tGPDprimitive *tgpi, tGPspoint *points2D) { BLI_assert(tgpi->tot_edges == 4); points2D[0].x = tgpi->top[0]; points2D[0].y = tgpi->top[1]; points2D[1].x = tgpi->bottom[0]; points2D[1].y = tgpi->top[1]; points2D[2].x = tgpi->bottom[0]; points2D[2].y = tgpi->bottom[1]; points2D[3].x = tgpi->top[0]; points2D[3].y = tgpi->bottom[1]; } /* create a line */ static void gp_primitive_line(tGPDprimitive *tgpi, tGPspoint *points2D) { BLI_assert(tgpi->tot_edges == 2); points2D[0].x = tgpi->top[0]; points2D[0].y = tgpi->top[1]; points2D[1].x = tgpi->bottom[0]; points2D[1].y = tgpi->bottom[1]; } /* create a circle */ static void gp_primitive_circle(tGPDprimitive *tgpi, tGPspoint *points2D) { const int totpoints = tgpi->tot_edges; const float step = (2.0f * M_PI) / (float)(totpoints); float center[2]; float radius[2]; float a = 0.0f; /* TODO: Use math-lib functions for these? */ center[0] = tgpi->top[0] + ((tgpi->bottom[0] - tgpi->top[0]) / 2.0f); center[1] = tgpi->top[1] + ((tgpi->bottom[1] - tgpi->top[1]) / 2.0f); radius[0] = fabsf(((tgpi->bottom[0] - tgpi->top[0]) / 2.0f)); radius[1] = fabsf(((tgpi->bottom[1] - tgpi->top[1]) / 2.0f)); for (int i = 0; i < totpoints; i++) { tGPspoint *p2d = &points2D[i]; p2d->x = (int)(center[0] + cosf(a) * radius[0]); p2d->y = (int)(center[1] + sinf(a) * radius[1]); a += step; } } /* Helper: Update shape of the stroke */ static void gp_primitive_update_strokes(bContext *C, tGPDprimitive *tgpi) { ToolSettings *ts = tgpi->scene->toolsettings; bGPdata *gpd = tgpi->gpd; bGPDstroke *gps = tgpi->gpf->strokes.first; /* realloc points to new size */ /* TODO: only do this if the size has changed? */ gps->points = MEM_reallocN(gps->points, sizeof(bGPDspoint) * tgpi->tot_edges); gps->dvert = MEM_reallocN(gps->dvert, sizeof(MDeformVert) * tgpi->tot_edges); gps->totpoints = tgpi->tot_edges; /* compute screen-space coordinates for points */ tGPspoint *points2D = MEM_callocN(sizeof(tGPspoint) * tgpi->tot_edges, "gp primitive points2D"); switch (tgpi->type) { case GP_STROKE_BOX: gp_primitive_rectangle(tgpi, points2D); break; case GP_STROKE_LINE: gp_primitive_line(tgpi, points2D); break; case GP_STROKE_CIRCLE: gp_primitive_circle(tgpi, points2D); break; default: break; } /* convert screen-coordinates to 3D coordinates */ for (int i = 0; i < gps->totpoints; i++) { bGPDspoint *pt = &gps->points[i]; MDeformVert *dvert = &gps->dvert[i]; tGPspoint *p2d = &points2D[i]; /* convert screen-coordinates to 3D coordinates */ gp_stroke_convertcoords_tpoint(tgpi->scene, tgpi->ar, tgpi->v3d, tgpi->ob, tgpi->gpl, p2d, NULL, &pt->x); pt->pressure = 1.0f; pt->strength = tgpi->brush->gpencil_settings->draw_strength; pt->time = 0.0f; dvert->totweight = 0; dvert->dw = NULL; } /* if axis locked, reproject to plane locked */ if (tgpi->lock_axis > GP_LOCKAXIS_NONE) { bGPDspoint *tpt = gps->points; float origin[3]; ED_gp_get_drawing_reference(tgpi->v3d, tgpi->scene, tgpi->ob, tgpi->gpl, ts->gpencil_v3d_align, origin); for (int i = 0; i < gps->totpoints; i++, tpt++) { ED_gp_project_point_to_plane(tgpi->ob, tgpi->rv3d, origin, ts->gp_sculpt.lock_axis - 1, tpt); } } /* if parented change position relative to parent object */ for (int i = 0; i < gps->totpoints; i++) { bGPDspoint *pt = &gps->points[i]; gp_apply_parent_point(tgpi->depsgraph, tgpi->ob, tgpi->gpd, tgpi->gpl, pt); } /* force fill recalc */ gps->flag |= GP_STROKE_RECALC_CACHES; /* free temp data */ MEM_SAFE_FREE(points2D); DEG_id_tag_update(&gpd->id, OB_RECALC_OB | OB_RECALC_DATA); WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, NULL); } /* Update screen and stroke */ static void gpencil_primitive_update(bContext *C, wmOperator *op, tGPDprimitive *tgpi) { /* update indicator in header */ gpencil_primitive_status_indicators(C, tgpi); /* apply... */ tgpi->type = RNA_enum_get(op->ptr, "type"); tgpi->tot_edges = RNA_int_get(op->ptr, "edges"); /* update points position */ gp_primitive_update_strokes(C, tgpi); } /* ----------------------- */ /* Exit and free memory */ static void gpencil_primitive_exit(bContext *C, wmOperator *op) { tGPDprimitive *tgpi = op->customdata; bGPdata *gpd = tgpi->gpd; /* don't assume that operator data exists at all */ if (tgpi) { /* remove drawing handler */ if (tgpi->draw_handle_3d) { ED_region_draw_cb_exit(tgpi->ar->type, tgpi->draw_handle_3d); } /* clear status message area */ ED_workspace_status_text(C, NULL); /* finally, free memory used by temp data */ BKE_gpencil_free_strokes(tgpi->gpf); MEM_freeN(tgpi->gpf); MEM_freeN(tgpi); } DEG_id_tag_update(&gpd->id, OB_RECALC_OB | OB_RECALC_DATA); WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, NULL); /* clear pointer */ op->customdata = NULL; } /* Init new temporary primitive data */ static void gpencil_primitive_init(bContext *C, wmOperator *op) { ToolSettings *ts = CTX_data_tool_settings(C); bGPdata *gpd = CTX_data_gpencil_data(C); Main *bmain = CTX_data_main(C); Scene *scene = CTX_data_scene(C); Depsgraph *depsgraph = CTX_data_depsgraph(C); int cfra_eval = (int)DEG_get_ctime(depsgraph); /* create temporary operator data */ tGPDprimitive *tgpi = MEM_callocN(sizeof(tGPDprimitive), "GPencil Primitive Data"); op->customdata = tgpi; /* set current scene and window info */ tgpi->scene = scene; tgpi->ob = CTX_data_active_object(C); tgpi->sa = CTX_wm_area(C); tgpi->ar = CTX_wm_region(C); tgpi->rv3d = tgpi->ar->regiondata; tgpi->v3d = tgpi->sa->spacedata.first; tgpi->depsgraph = CTX_data_depsgraph(C); tgpi->win = CTX_wm_window(C); /* set current frame number */ tgpi->cframe = cfra_eval; /* set GP datablock */ tgpi->gpd = gpd; /* getcolor info */ tgpi->mat = BKE_gpencil_material_ensure(bmain, tgpi->ob); /* set parameters */ tgpi->type = RNA_enum_get(op->ptr, "type"); /* if circle set default to 32 */ if (tgpi->type == GP_STROKE_CIRCLE) { RNA_int_set(op->ptr, "edges", 32); } else if(tgpi->type == GP_STROKE_BOX) { RNA_int_set(op->ptr, "edges", 4); } else { /* LINE */ RNA_int_set(op->ptr, "edges", 2); } tgpi->tot_edges = RNA_int_get(op->ptr, "edges"); tgpi->flag = IDLE; tgpi->lock_axis = ts->gp_sculpt.lock_axis; /* set temp layer, frame and stroke */ gp_primitive_set_initdata(C, tgpi); } /* ----------------------- */ /* Invoke handler: Initialize the operator */ static int gpencil_primitive_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event)) { wmWindow *win = CTX_wm_window(C); bGPdata *gpd = CTX_data_gpencil_data(C); tGPDprimitive *tgpi = NULL; /* initialize operator runtime data */ gpencil_primitive_init(C, op); tgpi = op->customdata; /* if in tools region, wait till we get to the main (3d-space) * region before allowing drawing to take place. */ op->flag |= OP_IS_MODAL_CURSOR_REGION; /* Enable custom drawing handlers */ tgpi->draw_handle_3d = ED_region_draw_cb_activate(tgpi->ar->type, gpencil_primitive_draw_3d, tgpi, REGION_DRAW_POST_VIEW); /* set cursor to indicate modal */ WM_cursor_modal_set(win, BC_CROSSCURSOR); /* update sindicator in header */ gpencil_primitive_status_indicators(C, tgpi); DEG_id_tag_update(&gpd->id, OB_RECALC_OB | OB_RECALC_DATA); WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, NULL); /* add a modal handler for this operator */ WM_event_add_modal_handler(C, op); return OPERATOR_RUNNING_MODAL; } /* Helper to complete a primitive */ static void gpencil_primitive_done(bContext *C, wmOperator *op, wmWindow *win, tGPDprimitive *tgpi) { bGPDframe *gpf; bGPDstroke *gps; /* return to normal cursor and header status */ ED_workspace_status_text(C, NULL); WM_cursor_modal_restore(win); /* insert keyframes as required... */ gpf = BKE_gpencil_layer_getframe(tgpi->gpl, tgpi->cframe, GP_GETFRAME_ADD_NEW); /* prepare stroke to get transfered */ gps = tgpi->gpf->strokes.first; if (gps) { gps->thickness = tgpi->brush->size; gps->flag |= GP_STROKE_RECALC_CACHES; } /* transfer stroke from temporary buffer to the actual frame */ BLI_movelisttolist(&gpf->strokes, &tgpi->gpf->strokes); BLI_assert(BLI_listbase_is_empty(&tgpi->gpf->strokes)); /* clean up temp data */ gpencil_primitive_exit(C, op); } /* Modal handler: Events handling during interactive part */ static int gpencil_primitive_modal(bContext *C, wmOperator *op, const wmEvent *event) { tGPDprimitive *tgpi = op->customdata; wmWindow *win = CTX_wm_window(C); const bool has_numinput = hasNumInput(&tgpi->num); switch (event->type) { case LEFTMOUSE: if ((event->val == KM_PRESS) && (tgpi->flag == IDLE)) { /* start drawing primitive */ /* TODO: Ignore if not in main region yet */ tgpi->flag = IN_PROGRESS; tgpi->top[0] = event->mval[0]; tgpi->top[1] = event->mval[1]; tgpi->bottom[0] = event->mval[0]; tgpi->bottom[1] = event->mval[1]; } else if ((event->val == KM_RELEASE) && (tgpi->flag == IN_PROGRESS)) { /* stop drawing primitive */ tgpi->flag = IDLE; gpencil_primitive_done(C, op, win, tgpi); /* done! */ return OPERATOR_FINISHED; } else { if (G.debug & G_DEBUG) { printf("GP Add Primitive Modal: LEFTMOUSE %d, Status = %d\n", event->val, tgpi->flag); } } break; case RETKEY: /* confirm */ { tgpi->flag = IDLE; gpencil_primitive_done(C, op, win, tgpi); /* done! */ return OPERATOR_FINISHED; } case ESCKEY: /* cancel */ case RIGHTMOUSE: { /* return to normal cursor and header status */ ED_workspace_status_text(C, NULL); WM_cursor_modal_restore(win); /* clean up temp data */ gpencil_primitive_exit(C, op); /* canceled! */ return OPERATOR_CANCELLED; } case WHEELUPMOUSE: { if (tgpi->type == GP_STROKE_CIRCLE) { tgpi->tot_edges = tgpi->tot_edges + 1; CLAMP(tgpi->tot_edges, MIN_EDGES, MAX_EDGES); RNA_int_set(op->ptr, "edges", tgpi->tot_edges); /* update screen */ gpencil_primitive_update(C, op, tgpi); } break; } case WHEELDOWNMOUSE: { if (tgpi->type == GP_STROKE_CIRCLE) { tgpi->tot_edges = tgpi->tot_edges - 1; CLAMP(tgpi->tot_edges, MIN_EDGES, MAX_EDGES); RNA_int_set(op->ptr, "edges", tgpi->tot_edges); /* update screen */ gpencil_primitive_update(C, op, tgpi); } break; } case MOUSEMOVE: /* calculate new position */ { /* only handle mousemove if not doing numinput */ if (has_numinput == false) { /* update position of mouse */ tgpi->bottom[0] = event->mval[0]; tgpi->bottom[1] = event->mval[1]; if (tgpi->flag == IDLE) { tgpi->top[0] = event->mval[0]; tgpi->top[1] = event->mval[1]; } /* Keep square if shift key */ if (event->shift) { tgpi->bottom[1] = tgpi->top[1] - (tgpi->bottom[0] - tgpi->top[0]); } /* update screen */ gpencil_primitive_update(C, op, tgpi); } break; } default: { if ((event->val == KM_PRESS) && handleNumInput(C, &tgpi->num, event)) { float value; /* Grab data from numeric input, and store this new value (the user see an int) */ value = tgpi->tot_edges; applyNumInput(&tgpi->num, &value); tgpi->tot_edges = value; CLAMP(tgpi->tot_edges, MIN_EDGES, MAX_EDGES); RNA_int_set(op->ptr, "edges", tgpi->tot_edges); /* update screen */ gpencil_primitive_update(C, op, tgpi); break; } else { /* unhandled event - allow to pass through */ return OPERATOR_RUNNING_MODAL | OPERATOR_PASS_THROUGH; } } } /* still running... */ return OPERATOR_RUNNING_MODAL; } /* Cancel handler */ static void gpencil_primitive_cancel(bContext *C, wmOperator *op) { /* this is just a wrapper around exit() */ gpencil_primitive_exit(C, op); } void GPENCIL_OT_primitive(wmOperatorType *ot) { static EnumPropertyItem primitive_type[] = { { GP_STROKE_BOX, "BOX", 0, "Box", "" }, { GP_STROKE_LINE, "LINE", 0, "Line", "" }, { GP_STROKE_CIRCLE, "CIRCLE", 0, "Circle", "" }, { 0, NULL, 0, NULL, NULL } }; /* identifiers */ ot->name = "Grease Pencil Shapes"; ot->idname = "GPENCIL_OT_primitive"; ot->description = "Create predefined grease pencil stroke shapes"; /* callbacks */ ot->invoke = gpencil_primitive_invoke; ot->modal = gpencil_primitive_modal; ot->cancel = gpencil_primitive_cancel; ot->poll = gpencil_primitive_add_poll; /* flags */ ot->flag = OPTYPE_UNDO | OPTYPE_BLOCKING; /* properties */ RNA_def_int(ot->srna, "edges", 4, MIN_EDGES, MAX_EDGES, "Edges", "Number of polygon edges", MIN_EDGES, MAX_EDGES); RNA_def_enum(ot->srna, "type", primitive_type, GP_STROKE_BOX, "Type", "Type of shape"); } /* *************************************************************** */