diff --git a/release/scripts/startup/bl_ui/space_view3d.py b/release/scripts/startup/bl_ui/space_view3d.py index 164f7682072..9c5b2371c0b 100644 --- a/release/scripts/startup/bl_ui/space_view3d.py +++ b/release/scripts/startup/bl_ui/space_view3d.py @@ -5138,6 +5138,11 @@ class VIEW3D_MT_edit_gpencil(Menu): layout.separator() + # Assets + layout.menu("VIEW3D_MT_edit_gpencil_asset", text="Create Asset") + + layout.separator() + # Cut, Copy, Paste layout.operator("gpencil.duplicate_move", text="Duplicate") layout.operator("gpencil.stroke_split", text="Split") @@ -5228,6 +5233,14 @@ class VIEW3D_MT_edit_gpencil_point(Menu): layout.menu("VIEW3D_MT_gpencil_vertex_group") +class VIEW3D_MT_edit_gpencil_asset(Menu): + bl_label = "Create Asset" + + def draw(self, _context): + layout = self.layout + layout.operator_enum("gpencil.asset_create", "source") + + class VIEW3D_MT_weight_gpencil(Menu): bl_label = "Weights" @@ -7401,6 +7414,11 @@ class VIEW3D_MT_gpencil_edit_context_menu(Menu): col.menu("VIEW3D_MT_mirror", text="Mirror") col.menu("GPENCIL_MT_snap", text="Snap") + # Assets + col.separator() + + col.operator_menu_enum("gpencil.asset_create", "source", text="Create Asset") + col.separator() # Duplicate operators @@ -7453,6 +7471,11 @@ class VIEW3D_MT_gpencil_edit_context_menu(Menu): col.menu("VIEW3D_MT_mirror", text="Mirror") col.menu("VIEW3D_MT_snap", text="Snap") + # Assets + col.separator() + + col.operator_menu_enum("gpencil.asset_create", "source", text="Create Asset") + col.separator() # Duplicate operators @@ -8138,6 +8161,7 @@ classes = ( VIEW3D_MT_edit_gpencil, VIEW3D_MT_edit_gpencil_stroke, VIEW3D_MT_edit_gpencil_point, + VIEW3D_MT_edit_gpencil_asset, VIEW3D_MT_edit_gpencil_delete, VIEW3D_MT_edit_gpencil_showhide, VIEW3D_MT_weight_gpencil, diff --git a/source/blender/blenkernel/BKE_gpencil.h b/source/blender/blenkernel/BKE_gpencil.h index 55ca1c38af4..39d2118c574 100644 --- a/source/blender/blenkernel/BKE_gpencil.h +++ b/source/blender/blenkernel/BKE_gpencil.h @@ -328,6 +328,7 @@ struct bGPDcurve *BKE_gpencil_stroke_editcurve_new(int tot_curve_points); * \return True if layer is editable */ bool BKE_gpencil_layer_is_editable(const struct bGPDlayer *gpl); +void BKE_gpencil_frame_min_max(const struct bGPdata *gpd, int *r_min, int *r_max); /* How gpencil_layer_getframe() should behave when there * is no existing GP-Frame on the frame requested. diff --git a/source/blender/blenkernel/intern/gpencil.c b/source/blender/blenkernel/intern/gpencil.c index 67b52f67cfc..5cafb6bbbc5 100644 --- a/source/blender/blenkernel/intern/gpencil.c +++ b/source/blender/blenkernel/intern/gpencil.c @@ -60,7 +60,7 @@ static CLG_LogRef LOG = {"bke.gpencil"}; static void greasepencil_copy_data(Main *UNUSED(bmain), ID *id_dst, const ID *id_src, - const int UNUSED(flag)) + const int flag) { bGPdata *gpd_dst = (bGPdata *)id_dst; const bGPdata *gpd_src = (const bGPdata *)id_src; @@ -108,6 +108,13 @@ static void greasepencil_copy_data(Main *UNUSED(bmain), BLI_addtail(&gpd_dst->layers, gpl_dst); } + + if (flag & LIB_ID_COPY_NO_PREVIEW) { + gpd_dst->preview = NULL; + } + else { + BKE_previewimg_id_copy(&gpd_dst->id, &gpd_src->id); + } } static void greasepencil_free_data(ID *id) @@ -177,6 +184,8 @@ static void greasepencil_blend_write(BlendWriter *writer, ID *id, const void *id } } } + + BKE_previewimg_blend_write(writer, gpd->preview); } } @@ -191,6 +200,10 @@ void BKE_gpencil_blend_read_data(BlendDataReader *reader, bGPdata *gpd) BLO_read_data_address(reader, &gpd->adt); BKE_animdata_blend_read_data(reader, gpd->adt); + /* Preview. */ + BLO_read_data_address(reader, &gpd->preview); + BKE_previewimg_blend_read(reader, gpd->preview); + /* Ensure full objectmode for linked grease pencil. */ if (ID_IS_LINKED(gpd)) { gpd->flag &= ~GP_DATA_STROKE_PAINTMODE; @@ -493,6 +506,8 @@ void BKE_gpencil_free_data(bGPdata *gpd, bool free_all) /* clear cache */ BKE_gpencil_batch_cache_free(gpd); } + /* Preview. */ + BKE_previewimg_free(&gpd->preview); } void BKE_gpencil_eval_delete(bGPdata *gpd_eval) @@ -2213,7 +2228,7 @@ int BKE_gpencil_object_material_index_get_by_name(Object *ob, const char *name) for (short i = 0; i < *totcol; i++) { read_ma = BKE_object_material_get(ob, i + 1); /* Material names are like "MAMaterial.001" */ - if (STREQ(name, &read_ma->id.name[2])) { + if ((read_ma) && (STREQ(name, &read_ma->id.name[2]))) { return i; } } @@ -3059,4 +3074,20 @@ void BKE_gpencil_update_on_write(bGPdata *gpd_orig, bGPdata *gpd_eval) BKE_gpencil_free_update_cache(gpd_orig); } +/* Get min and max frame number for all layers. */ +void BKE_gpencil_frame_min_max(const bGPdata *gpd, int *r_min, int *r_max) +{ + *r_min = INT_MAX; + *r_max = INT_MIN; + LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { + LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) { + if (gpf->framenum < *r_min) { + *r_min = gpf->framenum; + } + if (gpf->framenum > *r_max) { + *r_max = gpf->framenum; + } + } + } +} /** \} */ diff --git a/source/blender/blenkernel/intern/icons.cc b/source/blender/blenkernel/intern/icons.cc index 7fd0515b52c..3f8ea5b2905 100644 --- a/source/blender/blenkernel/intern/icons.cc +++ b/source/blender/blenkernel/intern/icons.cc @@ -355,6 +355,7 @@ PreviewImage **BKE_previewimg_id_get_p(const ID *id) ID_PRV_CASE(ID_LA, Light); ID_PRV_CASE(ID_IM, Image); ID_PRV_CASE(ID_BR, Brush); + ID_PRV_CASE(ID_GD, bGPdata); ID_PRV_CASE(ID_GR, Collection); ID_PRV_CASE(ID_SCE, Scene); ID_PRV_CASE(ID_SCR, bScreen); @@ -419,7 +420,7 @@ void BKE_previewimg_id_custom_set(ID *id, const char *filepath) bool BKE_previewimg_id_supports_jobs(const ID *id) { - return ELEM(GS(id->name), ID_OB, ID_MA, ID_TE, ID_LA, ID_WO, ID_IM, ID_BR, ID_GR); + return ELEM(GS(id->name), ID_OB, ID_MA, ID_TE, ID_LA, ID_WO, ID_IM, ID_BR, ID_GR, ID_GD); } void BKE_previewimg_deferred_release(PreviewImage *prv) diff --git a/source/blender/editors/asset/ED_asset_type.h b/source/blender/editors/asset/ED_asset_type.h index f0cddbdf070..0a72c6256a8 100644 --- a/source/blender/editors/asset/ED_asset_type.h +++ b/source/blender/editors/asset/ED_asset_type.h @@ -16,7 +16,8 @@ struct ID; bool ED_asset_type_id_is_non_experimental(const struct ID *id); #define ED_ASSET_TYPE_IDS_NON_EXPERIMENTAL_FLAGS \ - (FILTER_ID_MA | FILTER_ID_GR | FILTER_ID_OB | FILTER_ID_AC | FILTER_ID_WO | FILTER_ID_NT) + (FILTER_ID_MA | FILTER_ID_GR | FILTER_ID_OB | FILTER_ID_AC | FILTER_ID_WO | FILTER_ID_NT | \ + FILTER_ID_GD) /** * Check if the asset type for \a id (which doesn't need to be an asset right now) can be an asset, @@ -39,7 +40,7 @@ int64_t ED_asset_types_supported_as_filter_flags(void); * Should start with a consonant, so usages can prefix it with "a" (not "an"). */ #define ED_ASSET_TYPE_IDS_NON_EXPERIMENTAL_UI_STRING \ - "Material, Collection, Object, Pose Action, Node Group or World" + "Material, Collection, Object, Pose Action, Node Group or World, Grease Pencil" #ifdef __cplusplus } diff --git a/source/blender/editors/asset/intern/asset_type.cc b/source/blender/editors/asset/intern/asset_type.cc index 3ecf7df068f..f9d7aa7f0f0 100644 --- a/source/blender/editors/asset/intern/asset_type.cc +++ b/source/blender/editors/asset/intern/asset_type.cc @@ -16,7 +16,7 @@ bool ED_asset_type_id_is_non_experimental(const ID *id) { /* Remember to update #ED_ASSET_TYPE_IDS_NON_EXPERIMENTAL_UI_STRING and * #ED_ASSET_TYPE_IDS_NON_EXPERIMENTAL_FLAGS() with this! */ - return ELEM(GS(id->name), ID_MA, ID_GR, ID_OB, ID_AC, ID_WO, ID_NT); + return ELEM(GS(id->name), ID_MA, ID_GR, ID_OB, ID_AC, ID_WO, ID_NT, ID_GD); } bool ED_asset_type_is_supported(const ID *id) diff --git a/source/blender/editors/gpencil/CMakeLists.txt b/source/blender/editors/gpencil/CMakeLists.txt index af2ad0b7536..df1f3d9481a 100644 --- a/source/blender/editors/gpencil/CMakeLists.txt +++ b/source/blender/editors/gpencil/CMakeLists.txt @@ -2,6 +2,7 @@ set(INC ../include + ../../asset_system ../../blenfont ../../blenkernel ../../blenlib @@ -31,6 +32,7 @@ set(SRC gpencil_add_monkey.c gpencil_add_stroke.c gpencil_armature.c + gpencil_asset.cc gpencil_bake_animation.cc gpencil_convert.c gpencil_data.c diff --git a/source/blender/editors/gpencil/gpencil_asset.cc b/source/blender/editors/gpencil/gpencil_asset.cc new file mode 100644 index 00000000000..cb4d70adec7 --- /dev/null +++ b/source/blender/editors/gpencil/gpencil_asset.cc @@ -0,0 +1,816 @@ +/* SPDX-License-Identifier: GPL-2.0-or-later + * Copyright 2022 Blender Foundation. */ + +/** \file + * \ingroup edgpencil + */ + +#include "BLI_blenlib.h" +#include "BLI_math.h" +#include "BLI_utildefines.h" +#include "BLI_vector.hh" + +#include "BLT_translation.h" + +#include "MEM_guardedalloc.h" + +#include "DNA_gpencil_types.h" +#include "DNA_material_types.h" + +#include "BKE_asset.h" +#include "BKE_context.h" +#include "BKE_gpencil.h" +#include "BKE_gpencil_geom.h" +#include "BKE_lib_id.h" +#include "BKE_main.h" +#include "BKE_material.h" +#include "BKE_object.h" +#include "BKE_report.h" + +#include "UI_interface.h" +#include "UI_resources.h" + +#include "WM_api.h" +#include "WM_types.h" + +#include "RNA_access.h" +#include "RNA_define.h" +#include "RNA_enum_types.h" + +#include "GPU_immediate.h" + +#include "ED_asset.h" +#include "ED_gpencil.h" +#include "ED_keyframing.h" +#include "ED_screen.h" +#include "ED_space_api.h" + +#include "DEG_depsgraph.h" + +#include "gpencil_intern.h" + +typedef struct tGPDAssetStroke { + bGPDlayer *gpl; + bGPDframe *gpf; + bGPDstroke *gps; + int slot_index; + bool is_new_gpl; + bool is_new_gpf; +} tGPDAssetStroke; + +/* Temporary Asset import operation data. */ +typedef struct tGPDasset { + struct Main *bmain; + struct Depsgraph *depsgraph; + struct Scene *scene; + struct ScrArea *area; + struct ARegion *region; + /** Current object. */ + struct Object *ob; + /** Current GP data block. */ + struct bGPdata *gpd; + /** Asset GP data block. */ + struct bGPdata *gpd_asset; + /* Space Conversion Data */ + struct GP_SpaceConversion gsc; + + /** Current frame number. */ + int cframe; + + /** Drop initial position. */ + int drop[2]; + + /* Keep a reference of the asset data inserted in the target object. */ + blender::Vector asset_strokes; + +} tGPDasset; + +/* -------------------------------------------------------------------- */ +/** \name Create Grease Pencil data block Asset operator + * \{ */ + +typedef enum eGP_AssetSource { + /* Active Layer. */ + GP_ASSET_SOURCE_ACTIVE_LAYER = 0, + /* All Layers. */ + GP_ASSET_SOURCE_ALL_LAYERS, + /* All Layers in separated assets. */ + GP_ASSET_SOURCE_ALL_LAYERS_SPLIT, + /* Active Frame. */ + GP_ASSET_SOURCE_ACTIVE_KEYFRAME, + /* Active Frame All Layers. */ + GP_ASSET_SOURCE_ACTIVE_KEYFRAME_ALL_LAYERS, + /* Selected Frames. */ + GP_ASSET_SOURCE_SELECTED_KEYFRAMES, + /* Selected Strokes. */ + GP_ASSET_SOURCE_SELECTED_STROKES, + /* Selected Strokes. */ + GP_ASSET_SOURCE_SELECTED_POINTS, +} eGP_AssetSource; + +/* Helper: Apply layer settings. */ +static void apply_layer_settings(bGPDlayer *gpl) +{ + /* Apply layer attributes. */ + LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) { + LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) { + gps->fill_opacity_fac *= gpl->opacity; + gps->vert_color_fill[3] *= gpl->opacity; + for (int p = 0; p < gps->totpoints; p++) { + bGPDspoint *pt = &gps->points[p]; + float factor = (((float)gps->thickness * pt->pressure) + (float)gpl->line_change) / + ((float)gps->thickness * pt->pressure); + pt->pressure *= factor; + pt->strength *= gpl->opacity; + + /* Layer transformation. */ + mul_v3_m4v3(&pt->x, gpl->layer_mat, &pt->x); + zero_v3(gpl->location); + zero_v3(gpl->rotation); + copy_v3_fl(gpl->scale, 1.0f); + } + } + } + + gpl->line_change = 0; + gpl->opacity = 1.0f; + unit_m4(gpl->layer_mat); + invert_m4_m4(gpl->layer_invmat, gpl->layer_mat); +} + +/* Helper: Create an asset for data block. + * return: False if there are features non supported. */ +static bool gpencil_asset_create(const bContext *C, + const wmOperator *op, + const bGPdata *gpd_src, + const bGPDlayer *gpl_filter, + const eGP_AssetSource mode, + const bool reset_origin, + const bool flatten_layers) +{ + Main *bmain = CTX_data_main(C); + bool non_supported_feature = false; + const bool is_multiedit = (bool)GPENCIL_MULTIEDIT_SESSIONS_ON(gpd_src); + + /* Create a copy of selected data block. */ + bGPdata *gpd = reinterpret_cast(BKE_id_copy(bmain, &gpd_src->id)); + /* Enable fake user by default. */ + id_fake_user_set(&gpd->id); + /* Disable Edit mode. */ + gpd->flag &= ~GP_DATA_STROKE_EDITMODE; + + const bGPDlayer *gpl_active = BKE_gpencil_layer_active_get(gpd); + + bool is_animation = false; + + LISTBASE_FOREACH_MUTABLE (bGPDlayer *, gpl, &gpd->layers) { + /* If layer is hidden, remove. */ + if (gpl->flag & GP_LAYER_HIDE) { + BKE_gpencil_layer_delete(gpd, gpl); + continue; + } + + /* If Active Layer or Active Frame mode, delete non active layers. */ + if ((ELEM(mode, GP_ASSET_SOURCE_ACTIVE_LAYER, GP_ASSET_SOURCE_ACTIVE_KEYFRAME)) && + (gpl != gpl_active)) { + BKE_gpencil_layer_delete(gpd, gpl); + continue; + } + + /* For splitting, remove if layer is not equals to filter parameter. */ + if (mode == GP_ASSET_SOURCE_ALL_LAYERS_SPLIT) { + if (!STREQ(gpl_filter->info, gpl->info)) { + BKE_gpencil_layer_delete(gpd, gpl); + continue; + } + } + + /* Remove parenting data (feature non supported in data block). */ + if (gpl->parent != nullptr) { + gpl->parent = nullptr; + gpl->parsubstr[0] = 0; + gpl->partype = 0; + non_supported_feature = true; + } + + /* Remove masking (feature non supported in data block). */ + if (gpl->mask_layers.first) { + bGPDlayer_Mask *mask_next; + for (bGPDlayer_Mask *mask = static_cast(gpl->mask_layers.first); mask; + mask = mask_next) { + mask_next = mask->next; + BKE_gpencil_layer_mask_remove(gpl, mask); + } + gpl->mask_layers.first = nullptr; + gpl->mask_layers.last = nullptr; + + non_supported_feature = true; + } + + const bGPDframe *gpf_active = gpl->actframe; + + LISTBASE_FOREACH_MUTABLE (bGPDframe *, gpf, &gpl->frames) { + /* If Active Frame mode, delete non active frames or if multi frame edition is not enabled. + */ + if ((ELEM(mode, + GP_ASSET_SOURCE_ACTIVE_KEYFRAME, + GP_ASSET_SOURCE_ACTIVE_KEYFRAME_ALL_LAYERS) || + !is_multiedit) && + (gpf != gpf_active)) { + BKE_gpencil_layer_frame_delete(gpl, gpf); + continue; + } + + /* Remove if Selected frames mode and frame is not selected. */ + if ((mode == GP_ASSET_SOURCE_SELECTED_KEYFRAMES) && ((gpf->flag & GP_FRAME_SELECT) == 0)) { + BKE_gpencil_layer_frame_delete(gpl, gpf); + continue; + } + + /* Remove any unselected stroke if selected strokes mode. */ + if (ELEM(mode, GP_ASSET_SOURCE_SELECTED_STROKES, GP_ASSET_SOURCE_SELECTED_POINTS)) { + LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) { + if ((gps->flag & GP_STROKE_SELECT) == 0) { + BLI_remlink(&gpf->strokes, gps); + BKE_gpencil_free_stroke(gps); + continue; + } + } + } + /* Remove any unselected point if selected point mode. */ + if (mode == GP_ASSET_SOURCE_SELECTED_POINTS) { + LISTBASE_FOREACH_MUTABLE (bGPDstroke *, gps, &gpf->strokes) { + if (gps->flag & GP_STROKE_SELECT) { + /* Mark the points to dissolve */ + bGPDspoint *pt; + int i; + for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) { + if ((pt->flag & GP_SPOINT_SELECT) == 0) { + pt->flag |= GP_SPOINT_TAG; + } + } + BKE_gpencil_stroke_delete_tagged_points( + gpd, gpf, gps, gps->next, GP_SPOINT_TAG, false, false, 0); + } + } + } + + /* Unselect all strokes and points. */ + gpd->select_last_index = 0; + LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) { + gps->flag &= ~GP_STROKE_SELECT; + BKE_gpencil_stroke_select_index_reset(gps); + bGPDspoint *pt; + int i; + for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) { + pt->flag &= ~GP_SPOINT_SELECT; + } + } + + /* If Frame is empty, remove. */ + if (BLI_listbase_count(&gpf->strokes) == 0) { + BKE_gpencil_layer_frame_delete(gpl, gpf); + } + } + + /* If there are more than one frame in the same layer, then is an animation. */ + is_animation |= (BLI_listbase_count(&gpl->frames) > 1); + } + + /* Check if something to do. */ + bool do_export = false; + LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { + if (BLI_listbase_count(&gpl->frames) > 0) { + do_export = true; + break; + } + } + /* Nothing to export. */ + if (!do_export) { + BKE_report(op->reports, RPT_ERROR, "No strokes were found to create the asset"); + return false; + } + + /* Set origin to bounding box of strokes. */ + if (reset_origin) { + float gpcenter[3]; + BKE_gpencil_centroid_3d(gpd, gpcenter); + + LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { + LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) { + LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) { + bGPDspoint *pt; + int i; + for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) { + sub_v3_v3(&pt->x, gpcenter); + } + BKE_gpencil_stroke_boundingbox_calc(gps); + } + } + } + } + + /* Flatten layers. */ + if ((flatten_layers) && (gpd->layers.first)) { + /* Apply layer attributes to all layers. */ + LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { + apply_layer_settings(gpl); + } + + bGPDlayer *gpl_dst = static_cast(gpd->layers.first); + LISTBASE_FOREACH_BACKWARD_MUTABLE (bGPDlayer *, gpl, &gpd->layers) { + if (gpl == gpl_dst) { + break; + } + ED_gpencil_layer_merge(gpd, gpl, gpl->prev, false); + } + strcpy(gpl_dst->info, "Asset_Layer"); + } + + int f_min, f_max; + BKE_gpencil_frame_min_max(gpd, &f_min, &f_max); + + /* Mark as asset. */ + if (ED_asset_mark_id(&gpd->id)) { + ED_asset_generate_preview(C, &gpd->id); + /* Retime frame number to start by 1. Must be done after generate the render preview. */ + LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd->layers) { + LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) { + gpf->framenum -= f_min - 1; + } + } + } + + return non_supported_feature; +} + +static bool gpencil_asset_edit_poll(bContext *C) +{ + const enum eContextObjectMode mode = CTX_data_mode_enum(C); + + Object *ob = CTX_data_active_object(C); + if ((ob == nullptr) || (ob->type != OB_GPENCIL)) { + CTX_wm_operator_poll_msg_set(C, "Need a Grease Pencil object selected"); + return false; + } + + /* Only allowed in Grease Pencil Edit mode. */ + if (mode != CTX_MODE_EDIT_GPENCIL) { + CTX_wm_operator_poll_msg_set(C, "Grease Pencil object must be in Edit mode"); + return false; + } + + return ED_operator_view3d_active(C); +} + +static int gpencil_asset_create_exec(bContext *C, wmOperator *op) +{ + Object *ob = CTX_data_active_object(C); + bGPdata *gpd_src = static_cast(ob->data); + + const eGP_AssetSource source = static_cast(RNA_enum_get(op->ptr, "source")); + const bool reset_origin = RNA_boolean_get(op->ptr, "reset_origin"); + const bool flatten_layers = RNA_boolean_get(op->ptr, "flatten_layers"); + + bool non_supported_feature = false; + if (source == GP_ASSET_SOURCE_ALL_LAYERS_SPLIT) { + LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd_src->layers) { + non_supported_feature |= gpencil_asset_create( + C, op, gpd_src, gpl, source, reset_origin, flatten_layers); + } + } + else { + non_supported_feature = gpencil_asset_create( + C, op, gpd_src, nullptr, source, reset_origin, flatten_layers); + } + + /* Warnings for non supported features in the created asset. */ + if ((non_supported_feature) || (ob->greasepencil_modifiers.first) || (ob->shader_fx.first)) { + BKE_report(op->reports, + RPT_WARNING, + "Object has layer parenting, masking, modifiers or effects not supported in this " + "asset type. These features have been omitted in the asset"); + } + + WM_main_add_notifier(NC_ID | NA_EDITED, nullptr); + WM_main_add_notifier(NC_ASSET | NA_ADDED, nullptr); + + return OPERATOR_FINISHED; +} + +void GPENCIL_OT_asset_create(wmOperatorType *ot) +{ + static const EnumPropertyItem source_types[] = { + {GP_ASSET_SOURCE_ACTIVE_LAYER, + "LAYER", + 0, + "Active Layer", + "Create an asset using strokes of the active layer"}, + {GP_ASSET_SOURCE_ALL_LAYERS, + "LAYERS_ALL", + 0, + "All Layers", + "Create an asset using strokes from all layers"}, + {GP_ASSET_SOURCE_ALL_LAYERS_SPLIT, + "LAYERS_SPLIT", + 0, + "All Layers Separated", + "Create multiple grease pencil assets, one for each layer"}, + RNA_ENUM_ITEM_SEPR, + {GP_ASSET_SOURCE_ACTIVE_KEYFRAME, + "KEYFRAME", + 0, + "Active Keyframe (Active Layer)", + "Create an asset using active keyframe for active layer"}, + {GP_ASSET_SOURCE_ACTIVE_KEYFRAME_ALL_LAYERS, + "KEYFRAME_ALL", + 0, + "Active Keyframe (All Layers)", + "Create an asset using active keyframe for all layers"}, + {GP_ASSET_SOURCE_SELECTED_KEYFRAMES, + "KEYFRAME_SELECTED", + 0, + "Selected Keyframes", + "Create an asset using selected keyframes"}, + RNA_ENUM_ITEM_SEPR, + {GP_ASSET_SOURCE_SELECTED_STROKES, + "SELECTED", + 0, + "Selected Strokes", + "Create an asset using all selected strokes"}, + {GP_ASSET_SOURCE_SELECTED_POINTS, + "POINT", + 0, + "Selected Points", + "Create an asset using all selected points"}, + {0, nullptr, 0, nullptr, nullptr}, + }; + + /* identifiers */ + ot->name = "Create Grease Pencil Asset"; + ot->idname = "GPENCIL_OT_asset_create"; + ot->description = "Create asset from sections of the active object"; + + /* callbacks */ + ot->invoke = WM_menu_invoke; + ot->exec = gpencil_asset_create_exec; + ot->poll = gpencil_asset_edit_poll; + + /* flags */ + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + /* properties */ + ot->prop = RNA_def_enum( + ot->srna, "source", source_types, GP_ASSET_SOURCE_SELECTED_STROKES, "Create From", ""); + RNA_def_boolean(ot->srna, + "reset_origin", + true, + "Origin to Geometry", + "Set origin of the asset in the center of the strokes bounding box"); + RNA_def_boolean( + ot->srna, "flatten_layers", false, "Flatten Layers", "Merge all layers in only one"); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ +/** \name Import Grease Pencil Asset into existing data block operator + * \{ */ + +/* Helper: Get a material from the data block array. */ +static Material *gpencil_asset_material_get_from_id(ID *id, const int slot_index) +{ + const short *tot_slots_data_ptr = BKE_id_material_len_p(id); + const int tot_slots_data = tot_slots_data_ptr ? *tot_slots_data_ptr : 0; + if (slot_index >= tot_slots_data) { + return nullptr; + } + + Material ***materials_data_ptr = BKE_id_material_array_p(id); + Material **materials_data = materials_data_ptr ? *materials_data_ptr : nullptr; + Material *material = materials_data[slot_index]; + + return material; +} + +/* Helper: Set the selection of the imported strokes. */ +static void gpencil_asset_set_selection(tGPDasset *tgpa, const bool enable) +{ + + for (tGPDAssetStroke &data : tgpa->asset_strokes) { + bGPDframe *gpf = data.gpf; + if (enable) { + gpf->flag |= GP_FRAME_SELECT; + } + else { + gpf->flag &= ~GP_FRAME_SELECT; + } + + bGPDstroke *gps = data.gps; + if (enable) { + gps->flag |= GP_STROKE_SELECT; + } + else { + gps->flag &= ~GP_STROKE_SELECT; + } + + bGPDspoint *pt; + int i; + + for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) { + if (enable) { + pt->flag |= GP_SPOINT_SELECT; + } + else { + pt->flag &= ~GP_SPOINT_SELECT; + } + } + + /* Set selection index. */ + if (enable) { + gps->flag |= GP_STROKE_SELECT; + BKE_gpencil_stroke_select_index_set(tgpa->gpd, gps); + } + else { + gps->flag &= ~GP_STROKE_SELECT; + BKE_gpencil_stroke_select_index_reset(gps); + } + } +} + +/* Helper: Append all strokes from the asset in the target data block. */ +static bool gpencil_asset_append_strokes(tGPDasset *tgpa) +{ + bGPdata *gpd_target = tgpa->gpd; + bGPdata *gpd_asset = tgpa->gpd_asset; + + /* Get the vector from origin to drop position. */ + float dest_pt[3]; + float loc2d[2]; + copy_v2fl_v2i(loc2d, tgpa->drop); + gpencil_point_xy_to_3d(&tgpa->gsc, tgpa->scene, loc2d, dest_pt); + + float vec[3]; + sub_v3_v3v3(vec, dest_pt, tgpa->ob->loc); + + /* Verify something to do. */ + int data_len = 0; + LISTBASE_FOREACH (bGPDlayer *, gpl_asset, &gpd_asset->layers) { + if (data_len > 0) { + break; + } + LISTBASE_FOREACH (bGPDframe *, gpf_asset, &gpl_asset->frames) { + data_len += BLI_listbase_count(&gpf_asset->strokes); + if (data_len > 0) { + break; + } + } + } + + /* If the asset is empty, exit. */ + if (data_len == 0) { + return false; + } + + LISTBASE_FOREACH (bGPDlayer *, gpl_asset, &gpd_asset->layers) { + /* Check if Layer is in target data block. */ + bGPDlayer *gpl_target = BKE_gpencil_layer_get_by_name(gpd_target, gpl_asset->info, false); + + bool is_new_gpl = false; + if (gpl_target == nullptr) { + gpl_target = BKE_gpencil_layer_duplicate(gpl_asset, false, false); + BLI_assert(gpl_target != nullptr); + gpl_target->actframe = nullptr; + BLI_listbase_clear(&gpl_target->frames); + BLI_addtail(&gpd_target->layers, gpl_target); + is_new_gpl = true; + } + + LISTBASE_FOREACH (bGPDframe *, gpf_asset, &gpl_asset->frames) { + /* Check if frame is in target layer. */ + int fra = tgpa->cframe + (gpf_asset->framenum - 1); + bGPDframe *gpf_target = nullptr; + /* Find a frame in same frame number. */ + LISTBASE_FOREACH (bGPDframe *, gpf_find, &gpl_target->frames) { + if (gpf_find->framenum == fra) { + gpf_target = gpf_find; + break; + } + } + + bool is_new_gpf = false; + /* Check Rec button. If button is disabled, try to use active frame. + * If no active keyframe, must create a new frame. */ + if ((gpf_target == nullptr) && (!IS_AUTOKEY_ON(tgpa->scene))) { + gpf_target = BKE_gpencil_layer_frame_get(gpl_target, fra, GP_GETFRAME_USE_PREV); + } + + if (gpf_target == nullptr) { + gpf_target = BKE_gpencil_frame_addnew(gpl_target, fra); + gpl_target->actframe = gpf_target; + BLI_assert(gpf_target != nullptr); + BLI_listbase_clear(&gpf_target->strokes); + is_new_gpf = true; + } + + /* Loop all strokes and duplicate. */ + LISTBASE_FOREACH (bGPDstroke *, gps_asset, &gpf_asset->strokes) { + if (gps_asset->mat_nr == -1) { + continue; + } + + bGPDstroke *gps_target = BKE_gpencil_stroke_duplicate(gps_asset, true, true); + gps_target->next = gps_target->prev = nullptr; + gps_target->flag &= ~GP_STROKE_SELECT; + BLI_addtail(&gpf_target->strokes, gps_target); + + /* Add the material. */ + Material *ma_src = gpencil_asset_material_get_from_id(&tgpa->gpd_asset->id, + gps_asset->mat_nr); + + int mat_index = (ma_src != nullptr) ? BKE_gpencil_object_material_index_get_by_name( + tgpa->ob, ma_src->id.name + 2) : + -1; + bool is_new_mat = false; + if (mat_index == -1) { + const int totcolors = tgpa->ob->totcol; + mat_index = BKE_gpencil_object_material_ensure(tgpa->bmain, tgpa->ob, ma_src); + if (tgpa->ob->totcol > totcolors) { + is_new_mat = true; + } + } + + gps_target->mat_nr = mat_index; + + /* Apply the offset to drop position and unselect points. */ + bGPDspoint *pt; + int i; + for (i = 0, pt = gps_target->points; i < gps_target->totpoints; i++, pt++) { + add_v3_v3(&pt->x, vec); + pt->flag &= ~GP_SPOINT_SELECT; + } + + /* Calc stroke bounding box. */ + BKE_gpencil_stroke_boundingbox_calc(gps_target); + + /* Add the reference to the stroke. */ + int matidx = is_new_mat ? (gps_target->mat_nr + 1) : -1; + tGPDAssetStroke data = { + gpl_target, gpf_target, gps_target, matidx, is_new_gpl, is_new_gpf}; + tgpa->asset_strokes.append(data); + /* Reset flags. */ + is_new_gpl = false; + is_new_gpf = false; + } + } + } + + /* Unselect any frame and stroke. */ + LISTBASE_FOREACH (bGPDlayer *, gpl, &gpd_target->layers) { + LISTBASE_FOREACH (bGPDframe *, gpf, &gpl->frames) { + gpf->flag &= ~GP_FRAME_SELECT; + LISTBASE_FOREACH (bGPDstroke *, gps, &gpf->strokes) { + gps->flag &= ~GP_STROKE_SELECT; + bGPDspoint *pt; + int i; + for (i = 0, pt = gps->points; i < gps->totpoints; i++, pt++) { + pt->flag &= ~GP_SPOINT_SELECT; + } + } + } + } + + return true; +} + +/* Exit and free memory */ +static void gpencil_asset_import_exit(bContext *C, wmOperator *op) +{ + tGPDasset *tgpa = static_cast(op->customdata); + + if (tgpa) { + bGPdata *gpd = static_cast(tgpa->gpd); + + /* Free data. */ + MEM_delete(tgpa); + DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY); + } + + WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED | ND_DATA, nullptr); + + /* Clear pointer. */ + op->customdata = nullptr; +} + +/* Allocate memory and initialize values */ +static tGPDasset *gpencil_session_init_asset_import(bContext *C, wmOperator *op) +{ + Main *bmain = CTX_data_main(C); + ID *id = nullptr; + + PropertyRNA *prop_type = RNA_struct_find_property(op->ptr, "type"); + const short id_type = RNA_property_enum_get(op->ptr, prop_type); + id = WM_operator_properties_id_lookup_from_name_or_session_uuid( + bmain, op->ptr, (ID_Type)id_type); + if (id == nullptr) { + return nullptr; + } + const int object_type = BKE_object_obdata_to_type(id); + if (object_type != OB_GPENCIL) { + return nullptr; + } + + tGPDasset *tgpa = MEM_new(__func__); + + /* Save current settings. */ + tgpa->bmain = CTX_data_main(C); + tgpa->depsgraph = CTX_data_ensure_evaluated_depsgraph(C); + tgpa->scene = CTX_data_scene(C); + tgpa->area = CTX_wm_area(C); + tgpa->region = CTX_wm_region(C); + tgpa->ob = CTX_data_active_object(C); + + /* Setup space conversions data. */ + gpencil_point_conversion_init(C, &tgpa->gsc); + + /* Save current frame number. */ + tgpa->cframe = tgpa->scene->r.cfra; + + /* Target GP data block. */ + tgpa->gpd = static_cast(tgpa->ob->data); + /* Asset GP data block. */ + tgpa->gpd_asset = (bGPdata *)id; + + return tgpa; +} + +/* Init: Allocate memory and set init values */ +static bool gpencil_asset_import_init(bContext *C, wmOperator *op) +{ + op->customdata = static_cast(gpencil_session_init_asset_import(C, op)); + if (op->customdata == nullptr) { + gpencil_asset_import_exit(C, op); + return false; + } + + return true; +} + +/* Invoke handler: Initialize the operator. */ +static int gpencil_asset_import_invoke(bContext *C, wmOperator *op, const wmEvent *event) +{ + bGPdata *gpd = CTX_data_gpencil_data(C); + tGPDasset *tgpa = nullptr; + + /* Try to initialize context data needed. */ + if (!gpencil_asset_import_init(C, op)) { + if (op->customdata) { + MEM_delete(static_cast(op->customdata)); + } + return OPERATOR_CANCELLED; + } + tgpa = static_cast(op->customdata); + + /* Save initial position of drop. */ + tgpa->drop[0] = event->mval[0]; + tgpa->drop[1] = event->mval[1]; + + /* Load of the strokes in the target data block. */ + if (!gpencil_asset_append_strokes(tgpa)) { + gpencil_asset_import_exit(C, op); + BKE_report(op->reports, RPT_WARNING, "No strokes to append"); + return OPERATOR_FINISHED; + } + + /* Select imported strokes. */ + gpencil_asset_set_selection(tgpa, true); + /* Clean up temp data. */ + gpencil_asset_import_exit(C, op); + + DEG_id_tag_update(&gpd->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY); + WM_event_add_notifier(C, NC_GPENCIL | NA_EDITED, nullptr); + + return OPERATOR_FINISHED; +} + +void GPENCIL_OT_asset_import(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Grease Pencil Import Asset"; + ot->idname = "GPENCIL_OT_asset_import"; + ot->description = "Add strokes from a grease pencil asset to an existing grease pencil object"; + + /* callbacks */ + ot->invoke = gpencil_asset_import_invoke; + ot->poll = gpencil_asset_edit_poll; + + /* flags */ + ot->flag = OPTYPE_UNDO | OPTYPE_BLOCKING; + + /* Properties. */ + WM_operator_properties_id_lookup(ot, true); + PropertyRNA *prop = RNA_def_enum(ot->srna, "type", rna_enum_id_type_items, 0, "Type", ""); + RNA_def_property_translation_context(prop, BLT_I18NCONTEXT_ID_ID); + RNA_def_property_flag(prop, static_cast(PROP_SKIP_SAVE | PROP_HIDDEN)); +} +/** \} */ diff --git a/source/blender/editors/gpencil/gpencil_intern.h b/source/blender/editors/gpencil/gpencil_intern.h index 0614f2a748a..15fac19f27f 100644 --- a/source/blender/editors/gpencil/gpencil_intern.h +++ b/source/blender/editors/gpencil/gpencil_intern.h @@ -671,6 +671,10 @@ void GPENCIL_OT_convert_old_files(struct wmOperatorType *ot); /* armatures */ void GPENCIL_OT_generate_weights(struct wmOperatorType *ot); +/* Assets. */ +void GPENCIL_OT_asset_create(struct wmOperatorType *ot); +void GPENCIL_OT_asset_import(struct wmOperatorType *ot); + /* ****************************************************** */ /* Stroke Iteration Utilities */ diff --git a/source/blender/editors/gpencil/gpencil_ops.c b/source/blender/editors/gpencil/gpencil_ops.c index 85cc281ca90..bdb2490db02 100644 --- a/source/blender/editors/gpencil/gpencil_ops.c +++ b/source/blender/editors/gpencil/gpencil_ops.c @@ -687,6 +687,10 @@ void ED_operatortypes_gpencil(void) /* armatures */ WM_operatortype_append(GPENCIL_OT_generate_weights); + + /* Assets. */ + WM_operatortype_append(GPENCIL_OT_asset_create); + WM_operatortype_append(GPENCIL_OT_asset_import); } void ED_operatormacros_gpencil(void) diff --git a/source/blender/editors/render/render_preview.cc b/source/blender/editors/render/render_preview.cc index 57245558f70..4a6451f1b91 100644 --- a/source/blender/editors/render/render_preview.cc +++ b/source/blender/editors/render/render_preview.cc @@ -30,6 +30,7 @@ #include "DNA_brush_types.h" #include "DNA_camera_types.h" #include "DNA_collection_types.h" +#include "DNA_gpencil_types.h" #include "DNA_light_types.h" #include "DNA_material_types.h" #include "DNA_mesh_types.h" @@ -47,6 +48,7 @@ #include "BKE_colortools.h" #include "BKE_context.h" #include "BKE_global.h" +#include "BKE_gpencil.h" #include "BKE_icons.h" #include "BKE_idprop.h" #include "BKE_image.h" @@ -366,6 +368,7 @@ static ID *duplicate_ids(ID *id, const bool allow_failure) case ID_MA: case ID_TE: case ID_LA: + case ID_GD: case ID_WO: { BLI_assert(BKE_previewimg_id_supports_jobs(id)); ID *id_copy = BKE_id_copy_ex(nullptr, @@ -765,6 +768,8 @@ struct ObjectPreviewData { /* Copy of the object to create the preview for. The copy is for thread safety (and to insert * it into its own main). */ Object *object; + /* Datablock copy. Can be nullptr. */ + ID *datablock; /* Current frame. */ int cfra; int sizex; @@ -806,6 +811,10 @@ static Scene *object_preview_scene_create(const struct ObjectPreviewData *previe Depsgraph **r_depsgraph) { Scene *scene = BKE_scene_add(preview_data->pr_main, "Object preview scene"); + const bool is_gpencil = (preview_data->datablock != nullptr) && + (GS(preview_data->datablock->name) == ID_GD); + Object *ob_gpencil_temp = nullptr; + /* Preview need to be in the current frame to get a thumbnail similar of what * viewport displays. */ scene->r.cfra = preview_data->cfra; @@ -814,13 +823,37 @@ static Scene *object_preview_scene_create(const struct ObjectPreviewData *previe Depsgraph *depsgraph = DEG_graph_new( preview_data->pr_main, scene, view_layer, DAG_EVAL_VIEWPORT); - BLI_assert(preview_data->object != nullptr); - BLI_addtail(&preview_data->pr_main->objects, preview_data->object); + if (!is_gpencil) { + BLI_assert(preview_data->object != nullptr); + BLI_addtail(&preview_data->pr_main->objects, preview_data->object); - BKE_collection_object_add(preview_data->pr_main, scene->master_collection, preview_data->object); + BKE_collection_object_add( + preview_data->pr_main, scene->master_collection, preview_data->object); + } + else { + /* Grease pencil draw engine needs an object to draw the datablock. */ + ob_gpencil_temp = BKE_object_add_for_data(preview_data->pr_main, + scene, + view_layer, + OB_GPENCIL, + "preview_object", + preview_data->datablock, + true); + BLI_assert(ob_gpencil_temp != nullptr); + } + /* Copy the materials to get full color previews. */ + const short *materials_len_p = BKE_id_material_len_p(preview_data->datablock); + if (materials_len_p && *materials_len_p > 0) { + BKE_object_materials_test(preview_data->pr_main, + !is_gpencil ? preview_data->object : ob_gpencil_temp, + preview_data->datablock); + } - Object *camera_object = object_preview_camera_create( - preview_data->pr_main, scene, view_layer, preview_data->object); + Object *camera_object = object_preview_camera_create(preview_data->pr_main, + scene, + view_layer, + is_gpencil ? ob_gpencil_temp : + preview_data->object); scene->camera = camera_object; scene->r.xsch = preview_data->sizex; @@ -828,7 +861,8 @@ static Scene *object_preview_scene_create(const struct ObjectPreviewData *previe scene->r.size = 100; BKE_view_layer_synced_ensure(scene, view_layer); - Base *preview_base = BKE_view_layer_base_find(view_layer, preview_data->object); + Base *preview_base = BKE_view_layer_base_find( + view_layer, is_gpencil ? ob_gpencil_temp : preview_data->object); /* For 'view selected' below. */ preview_base->flag |= BASE_SELECTED; @@ -851,14 +885,34 @@ static void object_preview_render(IconPreview *preview, IconPreviewSize *preview BLI_assert(preview->id_copy && (preview->id_copy != preview->id)); + const bool is_gpencil = GS(preview->id->name) == ID_GD; + struct ObjectPreviewData preview_data = {}; preview_data.pr_main = preview_main; /* Act on a copy. */ - preview_data.object = (Object *)preview->id_copy; - preview_data.cfra = preview->scene->r.cfra; + if (!is_gpencil) { + preview_data.object = (Object *)preview->id_copy; + preview_data.datablock = nullptr; + } + else { + preview_data.object = nullptr; + preview_data.datablock = (ID *)preview->id_copy; + preview_data.cfra = preview->scene->r.cfra; + } preview_data.sizex = preview_sized->sizex; preview_data.sizey = preview_sized->sizey; + /* Grease Pencil needs to find the frame number to make preview visible. */ + if (is_gpencil) { + int f_min, f_max; + bGPdata *gpd = (bGPdata *)preview->id_copy; + BKE_gpencil_frame_min_max(gpd, &f_min, &f_max); + const int framenum = ((preview->scene->r.cfra < f_min) || (preview->scene->r.cfra > f_max)) ? + f_min : + preview->scene->r.cfra; + preview_data.cfra = framenum; + } + Depsgraph *depsgraph; Scene *scene = object_preview_scene_create(&preview_data, &depsgraph); @@ -1033,7 +1087,7 @@ static void action_preview_render(IconPreview *preview, IconPreviewSize *preview } /** \} */ - +/* -------------------------------------------------------------------- */ /* -------------------------------------------------------------------- */ /** \name New Shader Preview System * \{ */ @@ -1638,6 +1692,9 @@ static void icon_preview_startjob_all_sizes(void *customdata, case ID_AC: action_preview_render(ip, cur_size); continue; + case ID_GD: + object_preview_render(ip, cur_size); + continue; default: /* Fall through to the same code as the `ip->id == nullptr` case. */ break; diff --git a/source/blender/editors/space_view3d/space_view3d.cc b/source/blender/editors/space_view3d/space_view3d.cc index 25708ee7a67..a9904b8ddd5 100644 --- a/source/blender/editors/space_view3d/space_view3d.cc +++ b/source/blender/editors/space_view3d/space_view3d.cc @@ -633,6 +633,11 @@ static bool view3d_object_data_drop_poll(bContext *C, wmDrag *drag, const wmEven return false; } +static bool view3d_gpencil_drop_poll(bContext *C, wmDrag *drag, const wmEvent *event) +{ + return view3d_drop_id_in_main_region_poll(C, drag, event, ID_GD); +} + static char *view3d_object_data_drop_tooltip(bContext * /*C*/, wmDrag * /*drag*/, const int /*xy*/[2], @@ -641,6 +646,14 @@ static char *view3d_object_data_drop_tooltip(bContext * /*C*/, return BLI_strdup(TIP_("Create object instance from object-data")); } +static char *view3d_gpencil_data_drop_tooltip(bContext * /*C*/, + wmDrag * /*drag*/, + const int /*xy*/[2], + wmDropBox * /*drop*/) +{ + return BLI_strdup(TIP_("Add strokes to active object")); +} + static bool view3d_ima_drop_poll(bContext *C, wmDrag *drag, const wmEvent *event) { if (ED_region_overlap_isect_any_xy(CTX_wm_area(C), event->xy)) { @@ -1001,6 +1014,12 @@ static void view3d_dropboxes() view3d_id_path_drop_copy, WM_drag_free_imported_drag_ID, nullptr); + WM_dropbox_add(lb, + "GPENCIL_OT_asset_import", + view3d_gpencil_drop_poll, + view3d_id_drop_copy_with_type, + WM_drag_free_imported_drag_ID, + view3d_gpencil_data_drop_tooltip); WM_dropbox_add(lb, "OBJECT_OT_data_instance_add", view3d_object_data_drop_poll, @@ -1012,7 +1031,7 @@ static void view3d_dropboxes() view3d_world_drop_poll, view3d_id_drop_copy, WM_drag_free_imported_drag_ID, - nullptr); + NULL); } static void view3d_widgets() diff --git a/source/blender/makesdna/DNA_gpencil_types.h b/source/blender/makesdna/DNA_gpencil_types.h index 8b3f4956cfe..84b0f020dfc 100644 --- a/source/blender/makesdna/DNA_gpencil_types.h +++ b/source/blender/makesdna/DNA_gpencil_types.h @@ -759,6 +759,9 @@ typedef struct bGPdata { /* NOTE: When adding new members, make sure to add them to BKE_gpencil_data_copy_settings as * well! */ + /** Preview image for assets. */ + struct PreviewImage *preview; + bGPdata_Runtime runtime; } bGPdata;