From 509a7870c3570cbf8496bcee0c94cdc1e9e41df5 Mon Sep 17 00:00:00 2001 From: Jesse Yurkovich Date: Mon, 8 Apr 2024 22:10:39 +0200 Subject: [PATCH] Collection Exporters: Enable file exporters to be specified on Collections This implements the ability to have file exporters added and configured on Collections. Exporting is reachable from several locations: - Individually on each exporter configuration: The `Export` button in each panel header - For all exporters on the collection: The `Export All` button in the main panel interface - For all exporters on all collections in the scene: The `File`->`Export All Collections` button Visibility of which collections currently have exporters configured is done by ways of an icon added to the Collection row in the Outliner. Adding multiple exporters for the same file type is permitted. The user is free to setup several exports of the same format but with different file locations or settings etc. Notes: Only USD and Wavefront OBJ are enabled for the initial commit. Additional formats, including those implemented in Python will be added as separate commits after this. Ref #115690 Pull Request: https://projects.blender.org/blender/blender/pulls/116646 --- scripts/modules/bpy_types.py | 10 +- scripts/startup/bl_operators/presets.py | 21 ++ .../startup/bl_ui/properties_collection.py | 18 + scripts/startup/bl_ui/space_topbar.py | 3 + source/blender/blenkernel/BKE_collection.hh | 7 + source/blender/blenkernel/BKE_file_handler.hh | 7 + .../blender/blenkernel/intern/collection.cc | 47 +++ .../blender/blenkernel/intern/file_handler.cc | 5 + source/blender/blenkernel/intern/layer.cc | 7 + .../blender/editors/include/UI_interface_c.hh | 2 + .../templates/interface_templates.cc | 85 +++++ source/blender/editors/io/io_obj.cc | 9 +- source/blender/editors/io/io_usd.cc | 5 + .../editors/object/object_collection.cc | 331 ++++++++++++++++++ .../blender/editors/object/object_intern.hh | 2 + source/blender/editors/object/object_ops.cc | 1 + .../editors/space_outliner/outliner_draw.cc | 9 + .../blender/io/usd/intern/usd_capi_export.cc | 17 +- source/blender/io/usd/usd.hh | 1 + .../io/wavefront_obj/IO_wavefront_obj.hh | 1 + .../io/wavefront_obj/exporter/obj_exporter.cc | 35 +- .../io/wavefront_obj/exporter/obj_exporter.hh | 5 +- .../blender/makesdna/DNA_collection_types.h | 19 + source/blender/makesdna/DNA_layer_types.h | 1 + .../blender/makesrna/intern/rna_collection.cc | 53 +++ source/blender/makesrna/intern/rna_layer.cc | 8 + source/blender/makesrna/intern/rna_ui.cc | 9 +- source/blender/makesrna/intern/rna_ui_api.cc | 4 + 28 files changed, 712 insertions(+), 10 deletions(-) diff --git a/scripts/modules/bpy_types.py b/scripts/modules/bpy_types.py index d87261bed05..1b4808d731d 100644 --- a/scripts/modules/bpy_types.py +++ b/scripts/modules/bpy_types.py @@ -1099,7 +1099,7 @@ class Menu(StructRNA, _GenericUI, metaclass=RNAMeta): def path_menu(self, searchpaths, operator, *, props_default=None, prop_filepath="filepath", filter_ext=None, filter_path=None, display_name=None, - add_operator=None): + add_operator=None, add_operator_props=None): """ Populate a menu from a list of paths. @@ -1176,6 +1176,9 @@ class Menu(StructRNA, _GenericUI, metaclass=RNAMeta): props = row.operator(add_operator, text="", icon='REMOVE') props.name = name props.remove_name = True + if add_operator_props is not None: + for attr, value in add_operator_props.items(): + setattr(props, attr, value) if add_operator: wm = bpy.data.window_managers[0] @@ -1189,6 +1192,9 @@ class Menu(StructRNA, _GenericUI, metaclass=RNAMeta): props = row.operator(add_operator, text="", icon='ADD') props.name = wm.preset_name + if add_operator_props is not None: + for attr, value in add_operator_props.items(): + setattr(props, attr, value) def draw_preset(self, _context): """ @@ -1205,12 +1211,14 @@ class Menu(StructRNA, _GenericUI, metaclass=RNAMeta): ext_valid = getattr(self, "preset_extensions", {".py", ".xml"}) props_default = getattr(self, "preset_operator_defaults", None) add_operator = getattr(self, "preset_add_operator", None) + add_operator_props = getattr(self, "preset_add_operator_properties", None) self.path_menu( bpy.utils.preset_paths(self.preset_subdir), self.preset_operator, props_default=props_default, filter_ext=lambda ext: ext.lower() in ext_valid, add_operator=add_operator, + add_operator_props=add_operator_props, display_name=lambda name: bpy.path.display_name(name, title_case=False) ) diff --git a/scripts/startup/bl_operators/presets.py b/scripts/startup/bl_operators/presets.py index 49929cf5b42..4df8e6e4a53 100644 --- a/scripts/startup/bl_operators/presets.py +++ b/scripts/startup/bl_operators/presets.py @@ -7,6 +7,7 @@ from bpy.types import ( Menu, Operator, OperatorFileListElement, + Panel, WindowManager, ) from bpy.props import ( @@ -18,6 +19,7 @@ from bpy.app.translations import ( pgettext_rpt as rpt_, pgettext_data as data_, ) +from bl_ui.utils import PresetPanel # For preset popover menu @@ -750,6 +752,24 @@ class WM_MT_operator_presets(Menu): preset_operator = "script.execute_preset" +class WM_PT_operator_presets(PresetPanel, Panel): + bl_label = "Operator Presets" + preset_add_operator = "wm.operator_preset_add" + preset_operator = "script.execute_preset" + + @property + def preset_subdir(self): + return AddPresetOperator.operator_path(self.operator) + + @property + def preset_add_operator_properties(self): + return {"operator": self.operator} + + def draw(self, context): + self.operator = context.active_operator.bl_idname + PresetPanel.draw(self, context) + + class WM_OT_operator_presets_cleanup(Operator): """Remove outdated operator properties from presets that may cause problems""" @@ -921,5 +941,6 @@ classes = ( AddPresetEEVEERaytracing, ExecutePreset, WM_MT_operator_presets, + WM_PT_operator_presets, WM_OT_operator_presets_cleanup, ) diff --git a/scripts/startup/bl_ui/properties_collection.py b/scripts/startup/bl_ui/properties_collection.py index 80278781799..c5f7b4c7ab2 100644 --- a/scripts/startup/bl_ui/properties_collection.py +++ b/scripts/startup/bl_ui/properties_collection.py @@ -49,6 +49,23 @@ class COLLECTION_PT_collection_flags(CollectionButtonsPanel, Panel): col.prop(vlc, "indirect_only", toggle=False) +class COLLECTION_PT_exporters(CollectionButtonsPanel, Panel): + bl_label = "Exporters" + + def draw(self, context): + layout = self.layout + collection = context.collection + + row = layout.row() + col = row.column() + col.operator("wm.call_menu", text="Add", icon='ADD').name = "COLLECTION_MT_exporter_add" + col = row.column() + col.operator("COLLECTION_OT_export_all", icon='EXPORT') + col.enabled = len(collection.exporters) > 0 + + layout.template_collection_exporters() + + class COLLECTION_MT_context_menu_instance_offset(Menu): bl_label = "Instance Offset" @@ -114,6 +131,7 @@ classes = ( COLLECTION_PT_instancing, COLLECTION_PT_lineart_collection, COLLECTION_PT_collection_custom_props, + COLLECTION_PT_exporters, ) if __name__ == "__main__": # only for live edit. diff --git a/scripts/startup/bl_ui/space_topbar.py b/scripts/startup/bl_ui/space_topbar.py index ea82fd692ce..7b6ee9b2f8b 100644 --- a/scripts/startup/bl_ui/space_topbar.py +++ b/scripts/startup/bl_ui/space_topbar.py @@ -262,6 +262,9 @@ class TOPBAR_MT_file(Menu): layout.menu("TOPBAR_MT_file_import", icon='IMPORT') layout.menu("TOPBAR_MT_file_export", icon='EXPORT') + row = layout.row() + row.operator("wm.collection_export_all") + row.enabled = context.view_layer.has_export_collections layout.separator() diff --git a/source/blender/blenkernel/BKE_collection.hh b/source/blender/blenkernel/BKE_collection.hh index 761b279095a..2166a5d7fc6 100644 --- a/source/blender/blenkernel/BKE_collection.hh +++ b/source/blender/blenkernel/BKE_collection.hh @@ -23,6 +23,7 @@ struct BlendDataReader; struct BlendWriter; struct Collection; struct ID; +struct CollectionExport; struct Main; struct Object; struct Scene; @@ -64,6 +65,12 @@ void BKE_collection_add_from_collection(Main *bmain, * Free (or release) any data used by this collection (does not free the collection itself). */ void BKE_collection_free_data(Collection *collection); + +/** + * Free any data used by the IO handler (does not free the IO handler itself). + */ +void BKE_collection_exporter_free_data(CollectionExport *data); + /** * Remove a collection, optionally removing its child objects or moving * them to parent collections. diff --git a/source/blender/blenkernel/BKE_file_handler.hh b/source/blender/blenkernel/BKE_file_handler.hh index a66818c12b0..7020ad26fa2 100644 --- a/source/blender/blenkernel/BKE_file_handler.hh +++ b/source/blender/blenkernel/BKE_file_handler.hh @@ -23,6 +23,8 @@ struct FileHandlerType { char label[OP_MAX_TYPENAME]; /** Import operator name. */ char import_operator[OP_MAX_TYPENAME]; + /** Export operator name. */ + char export_operator[OP_MAX_TYPENAME]; /** Formatted string of file extensions supported by the file handler, each extension should * start with a `.` and be separated by `;`. For Example: `".blend;.ble"`. */ char file_extensions_str[FH_MAX_FILE_EXTENSIONS_STR]; @@ -40,6 +42,11 @@ struct FileHandlerType { * Return a vector of indices in #paths of file paths supported by the file handler. */ blender::Vector filter_supported_paths(const blender::Span paths) const; + + /** + * Generate a default file name for use with this file handler. + */ + std::string get_default_filename(const StringRefNull name); }; /** diff --git a/source/blender/blenkernel/intern/collection.cc b/source/blender/blenkernel/intern/collection.cc index 3d05bcad2f3..cd7eaac1c12 100644 --- a/source/blender/blenkernel/intern/collection.cc +++ b/source/blender/blenkernel/intern/collection.cc @@ -32,6 +32,7 @@ #include "BKE_main.hh" #include "BKE_object.hh" #include "BKE_preview_image.hh" +#include "BKE_report.hh" #include "BKE_rigidbody.h" #include "BKE_scene.hh" @@ -108,6 +109,7 @@ static void collection_gobject_hash_ensure(Collection *collection); static void collection_gobject_hash_update_object(Collection *collection, Object *ob_old, CollectionObject *cob); +static void collection_exporter_copy(Collection *collection, CollectionExport *data); /** \} */ @@ -159,6 +161,7 @@ static void collection_copy_data(Main *bmain, BLI_listbase_clear(&collection_dst->gobject); BLI_listbase_clear(&collection_dst->children); + BLI_listbase_clear(&collection_dst->exporters); BLI_listbase_clear(&collection_dst->runtime.parents); collection_dst->runtime.gobject_hash = nullptr; @@ -169,6 +172,9 @@ static void collection_copy_data(Main *bmain, LISTBASE_FOREACH (CollectionObject *, cob, &collection_src->gobject) { collection_object_add(bmain, collection_dst, cob->ob, &cob->light_linking, flag, false); } + LISTBASE_FOREACH (CollectionExport *, data, &collection_src->exporters) { + collection_exporter_copy(collection_dst, data); + } } static void collection_free_data(ID *id) @@ -187,6 +193,11 @@ static void collection_free_data(ID *id) BLI_freelistN(&collection->children); BLI_freelistN(&collection->runtime.parents); + LISTBASE_FOREACH (CollectionExport *, data, &collection->exporters) { + BKE_collection_exporter_free_data(data); + } + BLI_freelistN(&collection->exporters); + /* No need for depsgraph tagging here, since the data is being deleted. */ collection_object_cache_free(nullptr, collection, LIB_ID_CREATE_NO_DEG_TAG, 0); } @@ -274,6 +285,13 @@ void BKE_collection_blend_write_nolib(BlendWriter *writer, Collection *collectio LISTBASE_FOREACH (CollectionChild *, child, &collection->children) { BLO_write_struct(writer, CollectionChild, child); } + + LISTBASE_FOREACH (CollectionExport *, data, &collection->exporters) { + BLO_write_struct(writer, CollectionExport, data); + if (data->export_properties) { + IDP_BlendWrite(writer, data->export_properties); + } + } } static void collection_blend_write(BlendWriter *writer, ID *id, const void *id_address) @@ -324,6 +342,12 @@ void BKE_collection_blend_read_data(BlendDataReader *reader, Collection *collect BLO_read_list(reader, &collection->gobject); BLO_read_list(reader, &collection->children); + BLO_read_list(reader, &collection->exporters); + LISTBASE_FOREACH (CollectionExport *, data, &collection->exporters) { + BLO_read_data_address(reader, &data->export_properties); + IDP_BlendDataRead(reader, &data->export_properties); + } + BLO_read_data_address(reader, &collection->preview); BKE_previewimg_blend_read(reader, collection->preview); } @@ -490,6 +514,13 @@ void BKE_collection_free_data(Collection *collection) collection_free_data(&collection->id); } +void BKE_collection_exporter_free_data(struct CollectionExport *data) +{ + if (data->export_properties) { + IDP_FreeProperty(data->export_properties); + } +} + bool BKE_collection_delete(Main *bmain, Collection *collection, bool hierarchy) { /* Master collection is not real datablock, can't be removed. */ @@ -1353,6 +1384,22 @@ static bool collection_object_remove( return true; } +static void collection_exporter_copy(Collection *collection, CollectionExport *data) +{ + CollectionExport *new_data = MEM_cnew("CollectionExport"); + STRNCPY(new_data->fh_idname, data->fh_idname); + new_data->export_properties = IDP_CopyProperty(data->export_properties); + new_data->flag = data->flag; + + /* Clear the `filepath` property. */ + IDProperty *filepath = IDP_GetPropertyFromGroup(new_data->export_properties, "filepath"); + if (filepath) { + IDP_AssignString(filepath, ""); + } + + BLI_addtail(&collection->exporters, new_data); +} + bool BKE_collection_object_add_notest(Main *bmain, Collection *collection, Object *ob) { if (ob == nullptr) { diff --git a/source/blender/blenkernel/intern/file_handler.cc b/source/blender/blenkernel/intern/file_handler.cc index c4771ab7861..97d021d3606 100644 --- a/source/blender/blenkernel/intern/file_handler.cc +++ b/source/blender/blenkernel/intern/file_handler.cc @@ -123,4 +123,9 @@ blender::Vector FileHandlerType::filter_supported_paths( return indices; } +std::string FileHandlerType::get_default_filename(const StringRefNull name) +{ + return name + (file_extensions.is_empty() ? "" : file_extensions.first()); +} + } // namespace blender::bke diff --git a/source/blender/blenkernel/intern/layer.cc b/source/blender/blenkernel/intern/layer.cc index ea67d4c32c5..cdcf90498ac 100644 --- a/source/blender/blenkernel/intern/layer.cc +++ b/source/blender/blenkernel/intern/layer.cc @@ -1219,6 +1219,10 @@ static void layer_collection_sync(ViewLayer *view_layer, { child_layer->runtime_flag |= LAYER_COLLECTION_VISIBLE_VIEW_LAYER; } + + if (!BLI_listbase_is_empty(&child_collection->exporters)) { + view_layer->flag |= VIEW_LAYER_HAS_EXPORT_COLLECTIONS; + } } /* Replace layer collection list with new one. */ @@ -1356,6 +1360,9 @@ void BKE_layer_collection_sync(const Scene *scene, ViewLayer *view_layer) static_cast(view_layer->layer_collections.first), layer_resync_mempool); + /* Clear the cached flag indicating if the view layer has a collection exporter set. */ + view_layer->flag &= ~VIEW_LAYER_HAS_EXPORT_COLLECTIONS; + /* Generate new layer connections and object bases when collections changed. */ ListBase new_object_bases{}; const short parent_exclude = 0, parent_restrict = 0, parent_layer_restrict = 0; diff --git a/source/blender/editors/include/UI_interface_c.hh b/source/blender/editors/include/UI_interface_c.hh index 48493f03db2..d4cde172123 100644 --- a/source/blender/editors/include/UI_interface_c.hh +++ b/source/blender/editors/include/UI_interface_c.hh @@ -2709,6 +2709,8 @@ void uiTemplateNodeTreeInterface(uiLayout *layout, PointerRNA *ptr); */ void uiTemplateNodeInputs(uiLayout *layout, bContext *C, PointerRNA *ptr); +void uiTemplateCollectionExporters(uiLayout *layout, bContext *C); + /** * \return: True if the list item with unfiltered, unordered index \a item_idx is visible given the * current filter settings. diff --git a/source/blender/editors/interface/templates/interface_templates.cc b/source/blender/editors/interface/templates/interface_templates.cc index 2047a435dfc..c0adf21945d 100644 --- a/source/blender/editors/interface/templates/interface_templates.cc +++ b/source/blender/editors/interface/templates/interface_templates.cc @@ -35,6 +35,7 @@ #include "BLI_path_util.h" #include "BLI_rect.h" #include "BLI_string.h" +#include "BLI_string_ref.hh" #include "BLI_string_utils.hh" #include "BLI_time.h" #include "BLI_timecode.h" @@ -50,6 +51,7 @@ #include "BKE_constraint.h" #include "BKE_context.hh" #include "BKE_curveprofile.h" +#include "BKE_file_handler.hh" #include "BKE_global.hh" #include "BKE_gpencil_modifier_legacy.h" #include "BKE_idprop.hh" @@ -2962,6 +2964,89 @@ void uiTemplateOperatorRedoProperties(uiLayout *layout, const bContext *C) } } +static wmOperator *minimal_operator_create(wmOperatorType *ot, PointerRNA *properties) +{ + /* Copied from #wm_operator_create. + * Create a slimmed down operator suitable only for UI drawing. */ + wmOperator *op = MEM_cnew(ot->idname); + STRNCPY(op->idname, ot->idname); + op->type = ot; + + /* Initialize properties but do not assume ownership of them. + * This "minimal" operator owns nothing. */ + op->ptr = MEM_cnew("wmOperatorPtrRNA"); + op->properties = static_cast(properties->data); + *op->ptr = *properties; + + return op; +} + +static void draw_export_controls( + bContext *C, uiLayout *layout, const std::string &label, int index, bool valid) +{ + uiItemL(layout, label.c_str(), ICON_NONE); + if (valid) { + uiItemPopoverPanel(layout, C, "WM_PT_operator_presets", "", ICON_PRESET); + uiItemIntO(layout, "", ICON_EXPORT, "COLLECTION_OT_exporter_export", "index", index); + uiItemIntO(layout, "", ICON_X, "COLLECTION_OT_exporter_remove", "index", index); + } +} + +static void draw_export_properties(bContext *C, + uiLayout *layout, + wmOperator *op, + const std::string &filename) +{ + uiLayout *box = uiLayoutBox(layout); + + PropertyRNA *prop = RNA_struct_find_property(op->ptr, "filepath"); + std::string placeholder = "//" + filename; + uiItemFullR( + box, op->ptr, prop, RNA_NO_INDEX, 0, UI_ITEM_NONE, nullptr, ICON_NONE, placeholder.c_str()); + + template_operator_property_buts_draw_single(C, op, layout, UI_BUT_LABEL_ALIGN_NONE, 0); +} + +void uiTemplateCollectionExporters(uiLayout *layout, bContext *C) +{ + Collection *collection = CTX_data_collection(C); + ListBase *exporters = &collection->exporters; + + /* Draw all the IO handlers. */ + int index = 0; + LISTBASE_FOREACH_INDEX (CollectionExport *, data, exporters, index) { + using namespace blender; + PointerRNA exporter_ptr = RNA_pointer_create(&collection->id, &RNA_CollectionExport, data); + PanelLayout panel = uiLayoutPanelProp(C, layout, &exporter_ptr, "is_open"); + + bke::FileHandlerType *fh = bke::file_handler_find(data->fh_idname); + if (!fh) { + std::string label = std::string(IFACE_("Undefined")) + " " + data->fh_idname; + draw_export_controls(C, panel.header, label, index, false); + continue; + } + + wmOperatorType *ot = WM_operatortype_find(fh->export_operator, false); + if (!ot) { + std::string label = std::string(IFACE_("Undefined")) + " " + fh->export_operator; + draw_export_controls(C, panel.header, label, index, false); + continue; + } + + /* Assign temporary operator to uiBlock, which takes ownership. */ + PointerRNA properties = RNA_pointer_create(&collection->id, ot->srna, data->export_properties); + wmOperator *op = minimal_operator_create(ot, &properties); + UI_block_set_active_operator(uiLayoutGetBlock(panel.header), op, true); + + /* Draw panel header and contents. */ + std::string label(fh->label); + draw_export_controls(C, panel.header, label, index, true); + if (panel.body) { + draw_export_properties(C, panel.body, op, fh->get_default_filename(collection->id.name + 2)); + } + } +} + /** \} */ /* -------------------------------------------------------------------- */ diff --git a/source/blender/editors/io/io_obj.cc b/source/blender/editors/io/io_obj.cc index 5c211c6d53d..dd5bf6f30fe 100644 --- a/source/blender/editors/io/io_obj.cc +++ b/source/blender/editors/io/io_obj.cc @@ -108,6 +108,8 @@ static int wm_obj_export_exec(bContext *C, wmOperator *op) export_params.reports = op->reports; + RNA_string_get(op->ptr, "collection", export_params.collection); + OBJ_export(C, &export_params); return OPERATOR_FINISHED; @@ -197,8 +199,7 @@ static void ui_obj_export_settings(uiLayout *layout, PointerRNA *imfptr) static void wm_obj_export_draw(bContext * /*C*/, wmOperator *op) { - PointerRNA ptr = RNA_pointer_create(nullptr, op->type->srna, op->properties); - ui_obj_export_settings(op->layout, &ptr); + ui_obj_export_settings(op->layout, op->ptr); } /** @@ -387,6 +388,9 @@ void WM_OT_obj_export(wmOperatorType *ot) /* Only show .obj or .mtl files by default. */ prop = RNA_def_string(ot->srna, "filter_glob", "*.obj;*.mtl", 0, "Extension Filter", ""); RNA_def_property_flag(prop, PROP_HIDDEN); + + prop = RNA_def_string(ot->srna, "collection", nullptr, MAX_IDPROP_NAME, "Collection", nullptr); + RNA_def_property_flag(prop, PROP_HIDDEN); } static int wm_obj_import_exec(bContext *C, wmOperator *op) @@ -550,6 +554,7 @@ void obj_file_handler_add() auto fh = std::make_unique(); STRNCPY(fh->idname, "IO_FH_obj"); STRNCPY(fh->import_operator, "WM_OT_obj_import"); + STRNCPY(fh->export_operator, "WM_OT_obj_export"); STRNCPY(fh->label, "Wavefront OBJ"); STRNCPY(fh->file_extensions_str, ".obj"); fh->poll_drop = poll_file_object_drop; diff --git a/source/blender/editors/io/io_usd.cc b/source/blender/editors/io/io_usd.cc index 0c7ea023691..a002973b929 100644 --- a/source/blender/editors/io/io_usd.cc +++ b/source/blender/editors/io/io_usd.cc @@ -215,6 +215,7 @@ static int wm_usd_export_exec(bContext *C, wmOperator *op) }; STRNCPY(params.root_prim_path, root_prim_path); + RNA_string_get(op->ptr, "collection", params.collection); bool ok = USD_export(C, filepath, ¶ms, as_background_job, op->reports); @@ -348,6 +349,9 @@ void WM_OT_usd_export(wmOperatorType *ot) "Only export visible objects. Invisible parents of exported objects are " "exported as empty transforms"); + prop = RNA_def_string(ot->srna, "collection", nullptr, MAX_IDPROP_NAME, "Collection", nullptr); + RNA_def_property_flag(prop, PROP_HIDDEN); + RNA_def_boolean( ot->srna, "export_animation", @@ -832,6 +836,7 @@ void usd_file_handler_add() auto fh = std::make_unique(); STRNCPY(fh->idname, "IO_FH_usd"); STRNCPY(fh->import_operator, "WM_OT_usd_import"); + STRNCPY(fh->export_operator, "WM_OT_usd_export"); STRNCPY(fh->label, "Universal Scene Description"); STRNCPY(fh->file_extensions_str, ".usd;.usda;.usdc;.usdz"); fh->poll_drop = poll_file_object_drop; diff --git a/source/blender/editors/object/object_collection.cc b/source/blender/editors/object/object_collection.cc index 96ddd0737a0..ad550c8ce61 100644 --- a/source/blender/editors/object/object_collection.cc +++ b/source/blender/editors/object/object_collection.cc @@ -8,6 +8,8 @@ #include +#include "BLI_path_util.h" +#include "BLI_string.h" #include "BLI_utildefines.h" #include "DNA_collection_types.h" @@ -16,11 +18,16 @@ #include "BKE_collection.hh" #include "BKE_context.hh" +#include "BKE_file_handler.hh" +#include "BKE_idprop.hh" #include "BKE_layer.hh" #include "BKE_lib_id.hh" #include "BKE_main.hh" #include "BKE_object.hh" #include "BKE_report.hh" +#include "BKE_screen.hh" + +#include "BLT_translation.hh" #include "DEG_depsgraph.hh" #include "DEG_depsgraph_build.hh" @@ -36,6 +43,7 @@ #include "RNA_enum_types.hh" #include "RNA_prototypes.h" +#include "UI_interface.hh" #include "UI_interface_icons.hh" #include "object_intern.hh" @@ -416,6 +424,329 @@ void COLLECTION_OT_create(wmOperatorType *ot) ot->srna, "name", "Collection", MAX_ID_NAME - 2, "Name", "Name of the new collection"); } +static bool collection_exporter_poll(bContext *C) +{ + return CTX_data_collection(C) != nullptr; +} + +static bool collection_export_all_poll(bContext *C) +{ + return CTX_data_view_layer(C) != nullptr; +} + +static int collection_exporter_add_exec(bContext *C, wmOperator *op) +{ + using namespace blender; + Collection *collection = CTX_data_collection(C); + ListBase *exporters = &collection->exporters; + + char name[MAX_ID_NAME - 2]; /* id name */ + RNA_string_get(op->ptr, "name", name); + + bke::FileHandlerType *fh = bke::file_handler_find(name); + if (!fh) { + BKE_reportf(op->reports, RPT_ERROR, "File handler '%s' not found", name); + return OPERATOR_CANCELLED; + } + + if (!WM_operatortype_find(fh->export_operator, true)) { + BKE_reportf( + op->reports, RPT_ERROR, "File handler operator '%s' not found", fh->export_operator); + return OPERATOR_CANCELLED; + } + + /* Add a new #CollectionExport item to our handler list and fill it with #FileHandlerType + * information. Also load in the operator's properties now as well. */ + CollectionExport *data = MEM_cnew("CollectionExport"); + STRNCPY(data->fh_idname, fh->idname); + + IDPropertyTemplate val{}; + data->export_properties = IDP_New(IDP_GROUP, &val, "export_properties"); + data->flag |= IO_HANDLER_PANEL_OPEN; + + BLI_addtail(exporters, data); + + BKE_view_layer_need_resync_tag(CTX_data_view_layer(C)); + DEG_id_tag_update(&collection->id, ID_RECALC_SYNC_TO_EVAL); + + WM_event_add_notifier(C, NC_SPACE | ND_SPACE_PROPERTIES, nullptr); + WM_event_add_notifier(C, NC_SPACE | ND_SPACE_OUTLINER, nullptr); + + return OPERATOR_FINISHED; +} + +void COLLECTION_OT_exporter_add(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Add Exporter"; + ot->description = "Add Exporter"; + ot->idname = "COLLECTION_OT_exporter_add"; + + /* api callbacks */ + ot->exec = collection_exporter_add_exec; + ot->poll = collection_exporter_poll; + + /* flags */ + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + RNA_def_string(ot->srna, "name", nullptr, MAX_ID_NAME - 2, "Name", "FileHandler idname"); +} + +static int collection_exporter_remove_exec(bContext *C, wmOperator *op) +{ + Collection *collection = CTX_data_collection(C); + ListBase *exporters = &collection->exporters; + + int index = RNA_int_get(op->ptr, "index"); + CollectionExport *data = static_cast(BLI_findlink(exporters, index)); + if (!data) { + return OPERATOR_CANCELLED; + } + + BLI_remlink(exporters, data); + BKE_collection_exporter_free_data(data); + + MEM_freeN(data); + + BKE_view_layer_need_resync_tag(CTX_data_view_layer(C)); + DEG_id_tag_update(&collection->id, ID_RECALC_SYNC_TO_EVAL); + + WM_event_add_notifier(C, NC_SPACE | ND_SPACE_PROPERTIES, nullptr); + WM_event_add_notifier(C, NC_SPACE | ND_SPACE_OUTLINER, nullptr); + + return OPERATOR_FINISHED; +} + +static int collection_exporter_remove_invoke(bContext *C, + wmOperator *op, + const wmEvent * /*event*/) +{ + return WM_operator_confirm_ex( + C, op, IFACE_("Remove exporter?"), nullptr, IFACE_("Delete"), ALERT_ICON_NONE, false); +} + +void COLLECTION_OT_exporter_remove(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Remove Exporter"; + ot->description = "Remove Exporter"; + ot->idname = "COLLECTION_OT_exporter_remove"; + + /* api callbacks */ + ot->invoke = collection_exporter_remove_invoke; + ot->exec = collection_exporter_remove_exec; + ot->poll = collection_exporter_poll; + + /* flags */ + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + RNA_def_int(ot->srna, "index", 0, 0, INT_MAX, "Index", "Exporter index", 0, INT_MAX); +} + +static int collection_exporter_export(bContext *C, + wmOperator *op, + CollectionExport *data, + Collection *collection) +{ + using namespace blender; + bke::FileHandlerType *fh = bke::file_handler_find(data->fh_idname); + if (!fh) { + BKE_reportf(op->reports, RPT_ERROR, "File handler '%s' not found", data->fh_idname); + return OPERATOR_CANCELLED; + } + + wmOperatorType *ot = WM_operatortype_find(fh->export_operator, false); + if (!ot) { + BKE_reportf( + op->reports, RPT_ERROR, "File handler operator '%s' not found", fh->export_operator); + return OPERATOR_CANCELLED; + } + + /* Execute operator with our stored properties. */ + /* TODO: Cascade settings down from parent collections(?) */ + IDProperty *op_props = IDP_CopyProperty(data->export_properties); + PointerRNA properties = RNA_pointer_create(nullptr, ot->srna, op_props); + const char *collection_name = collection->id.name + 2; + + /* Ensure we have a valid filepath set. Create one if the user has not specified anything yet. */ + char filepath[FILE_MAX]; + RNA_string_get(&properties, "filepath", filepath); + if (!filepath[0]) { + BLI_path_join( + filepath, sizeof(filepath), "//", fh->get_default_filename(collection_name).c_str()); + } + else { + char filename[FILENAME_MAX]; + BLI_path_split_file_part(filepath, filename, sizeof(filename)); + if (!filename[0] || !BLI_path_extension(filename)) { + BKE_reportf(op->reports, RPT_ERROR, "File path '%s' is not a valid file", filepath); + + IDP_FreeProperty(op_props); + return OPERATOR_CANCELLED; + } + } + + const Main *bmain = CTX_data_main(C); + BLI_path_abs(filepath, BKE_main_blendfile_path(bmain)); + + RNA_string_set(&properties, "filepath", filepath); + RNA_string_set(&properties, "collection", collection_name); + int op_result = WM_operator_name_call_ptr(C, ot, WM_OP_EXEC_DEFAULT, &properties, nullptr); + + IDP_FreeProperty(op_props); + return op_result; +} + +static int collection_exporter_export_exec(bContext *C, wmOperator *op) +{ + Collection *collection = CTX_data_collection(C); + ListBase *exporters = &collection->exporters; + + int index = RNA_int_get(op->ptr, "index"); + CollectionExport *data = static_cast(BLI_findlink(exporters, index)); + if (!data) { + return OPERATOR_CANCELLED; + } + + return collection_exporter_export(C, op, data, collection); +} + +void COLLECTION_OT_exporter_export(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Export"; + ot->description = "Invoke the export operation"; + ot->idname = "COLLECTION_OT_exporter_export"; + + /* api callbacks */ + ot->exec = collection_exporter_export_exec; + ot->poll = collection_exporter_poll; + + /* flags */ + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + RNA_def_int(ot->srna, "index", 0, 0, INT_MAX, "Index", "Exporter index", 0, INT_MAX); +} + +static int collection_export(bContext *C, wmOperator *op, Collection *collection) +{ + ListBase *exporters = &collection->exporters; + + LISTBASE_FOREACH (CollectionExport *, data, exporters) { + if (collection_exporter_export(C, op, data, collection) != OPERATOR_FINISHED) { + /* Do not continue calling exporters if we encounter one that fails. */ + return OPERATOR_CANCELLED; + } + } + + return OPERATOR_FINISHED; +} + +static int collection_io_export_all_exec(bContext *C, wmOperator *op) +{ + Collection *collection = CTX_data_collection(C); + return collection_export(C, op, collection); +} + +void COLLECTION_OT_export_all(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Export All"; + ot->description = "Invoke all configured exporters on this collection"; + ot->idname = "COLLECTION_OT_export_all"; + + /* api callbacks */ + ot->exec = collection_io_export_all_exec; + ot->poll = collection_exporter_poll; + + /* flags */ + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; +} + +static int collection_export_recursive(bContext *C, + wmOperator *op, + LayerCollection *layer_collection) +{ + /* Skip collections which have been Excluded in the View Layer. */ + if (layer_collection->flag & LAYER_COLLECTION_EXCLUDE) { + return OPERATOR_FINISHED; + } + + if (collection_export(C, op, layer_collection->collection) != OPERATOR_FINISHED) { + return OPERATOR_CANCELLED; + } + + LISTBASE_FOREACH (LayerCollection *, child, &layer_collection->layer_collections) { + if (collection_export_recursive(C, op, child) != OPERATOR_FINISHED) { + return OPERATOR_CANCELLED; + } + } + + return OPERATOR_FINISHED; +} + +static int wm_collection_export_all_exec(bContext *C, wmOperator *op) +{ + ViewLayer *view_layer = CTX_data_view_layer(C); + LISTBASE_FOREACH (LayerCollection *, layer_collection, &view_layer->layer_collections) { + if (collection_export_recursive(C, op, layer_collection) != OPERATOR_FINISHED) { + return OPERATOR_CANCELLED; + } + } + + return OPERATOR_FINISHED; +} + +void WM_OT_collection_export_all(wmOperatorType *ot) +{ + /* identifiers */ + ot->name = "Export All Collections"; + ot->description = "Invoke all configured exporters for all collections"; + ot->idname = "WM_OT_collection_export_all"; + + /* api callbacks */ + ot->exec = wm_collection_export_all_exec; + ot->poll = collection_export_all_poll; + + /* flags */ + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; +} + +static void collection_exporter_menu_draw(const bContext * /*C*/, Menu *menu) +{ + using namespace blender; + uiLayout *layout = menu->layout; + + /* Add all file handlers capable of being exported to the menu. */ + bool at_least_one = false; + for (const auto &fh : bke::file_handlers()) { + if (WM_operatortype_find(fh->export_operator, true)) { + uiItemStringO( + layout, fh->label, ICON_NONE, "COLLECTION_OT_exporter_add", "name", fh->idname); + at_least_one = true; + } + } + + if (!at_least_one) { + uiItemL(layout, IFACE_("No file handlers available"), ICON_NONE); + } +} + +void collection_exporter_register() +{ + MenuType *mt = MEM_cnew(__func__); + STRNCPY(mt->idname, "COLLECTION_MT_exporter_add"); + STRNCPY(mt->label, N_("Add Exporter")); + mt->draw = collection_exporter_menu_draw; + + WM_menutype_add(mt); + WM_operatortype_append(COLLECTION_OT_exporter_add); + WM_operatortype_append(COLLECTION_OT_exporter_remove); + WM_operatortype_append(COLLECTION_OT_exporter_export); + WM_operatortype_append(COLLECTION_OT_export_all); + WM_operatortype_append(WM_OT_collection_export_all); +} + /****************** properties window operators *********************/ static int collection_add_exec(bContext *C, wmOperator * /*op*/) diff --git a/source/blender/editors/object/object_intern.hh b/source/blender/editors/object/object_intern.hh index a5cdaca57da..0abd7cf7e84 100644 --- a/source/blender/editors/object/object_intern.hh +++ b/source/blender/editors/object/object_intern.hh @@ -377,4 +377,6 @@ void OBJECT_OT_datalayout_transfer(wmOperatorType *ot); void object_modifier_add_asset_register(); +void collection_exporter_register(); + } // namespace blender::ed::object diff --git a/source/blender/editors/object/object_ops.cc b/source/blender/editors/object/object_ops.cc index 71b67bb0192..588c79046c2 100644 --- a/source/blender/editors/object/object_ops.cc +++ b/source/blender/editors/object/object_ops.cc @@ -303,6 +303,7 @@ void operatortypes_object() WM_operatortype_append(OBJECT_OT_light_linking_unlink_from_collection); object_modifier_add_asset_register(); + collection_exporter_register(); } void operatormacros_object() diff --git a/source/blender/editors/space_outliner/outliner_draw.cc b/source/blender/editors/space_outliner/outliner_draw.cc index 52ae3632c41..a6a5bbda52b 100644 --- a/source/blender/editors/space_outliner/outliner_draw.cc +++ b/source/blender/editors/space_outliner/outliner_draw.cc @@ -3442,6 +3442,15 @@ static void outliner_draw_tree_element(bContext *C, float(startx) + offsx + 2 * ufac, float(*starty) + 2 * ufac, lib_icon, alpha_fac); offsx += UI_UNIT_X + 4 * ufac; } + + if (tselem->type == TSE_LAYER_COLLECTION) { + const Collection *collection = (Collection *)tselem->id; + if (!BLI_listbase_is_empty(&collection->exporters)) { + UI_icon_draw_alpha( + float(startx) + offsx + 2 * ufac, float(*starty) + 2 * ufac, ICON_EXPORT, alpha_fac); + offsx += UI_UNIT_X + 4 * ufac; + } + } } GPU_blend(GPU_BLEND_NONE); diff --git a/source/blender/io/usd/intern/usd_capi_export.cc b/source/blender/io/usd/intern/usd_capi_export.cc index c3fc350a609..4f916c04626 100644 --- a/source/blender/io/usd/intern/usd_capi_export.cc +++ b/source/blender/io/usd/intern/usd_capi_export.cc @@ -25,12 +25,14 @@ #include "DEG_depsgraph_build.hh" #include "DEG_depsgraph_query.hh" +#include "DNA_collection_types.h" #include "DNA_scene_types.h" #include "BKE_appdir.hh" #include "BKE_blender_version.h" #include "BKE_context.hh" #include "BKE_global.hh" +#include "BKE_lib_id.hh" #include "BKE_report.hh" #include "BKE_scene.hh" @@ -483,7 +485,20 @@ bool USD_export(bContext *C, * * Has to be done from main thread currently, as it may affect Main original data (e.g. when * doing deferred update of the view-layers, see #112534 for details). */ - if (job->params.visible_objects_only) { + if (strlen(job->params.collection) > 0) { + Collection *collection = reinterpret_cast( + BKE_libblock_find_name(job->bmain, ID_GR, job->params.collection)); + if (!collection) { + BKE_reportf(job->params.worker_status->reports, + RPT_ERROR, + "USD Export: Unable to find collection %s", + job->params.collection); + return false; + } + + DEG_graph_build_from_collection(job->depsgraph, collection); + } + else if (job->params.visible_objects_only) { DEG_graph_build_from_view_layer(job->depsgraph); } else { diff --git a/source/blender/io/usd/usd.hh b/source/blender/io/usd/usd.hh index e6228079a63..d668a1a74eb 100644 --- a/source/blender/io/usd/usd.hh +++ b/source/blender/io/usd/usd.hh @@ -85,6 +85,7 @@ struct USDExportParams { bool overwrite_textures = true; bool relative_paths = true; char root_prim_path[1024] = ""; /* FILE_MAX */ + char collection[MAX_IDPROP_NAME] = ""; /** Communication structure between the wmJob management code and the worker code. Currently used * to generate safely reports from the worker thread. */ diff --git a/source/blender/io/wavefront_obj/IO_wavefront_obj.hh b/source/blender/io/wavefront_obj/IO_wavefront_obj.hh index 367e4513ee2..ab74efc0142 100644 --- a/source/blender/io/wavefront_obj/IO_wavefront_obj.hh +++ b/source/blender/io/wavefront_obj/IO_wavefront_obj.hh @@ -23,6 +23,7 @@ struct OBJExportParams { char filepath[FILE_MAX]; /** Pretend that destination file folder is this, if non-empty. Used only for tests. */ char file_base_for_tests[FILE_MAX]; + char collection[MAX_IDPROP_NAME] = ""; /** Full path to current blender file (used for comments in output). */ const char *blen_filepath; diff --git a/source/blender/io/wavefront_obj/exporter/obj_exporter.cc b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc index 11ecde162fe..f24ef0fc92f 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_exporter.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.cc @@ -11,6 +11,7 @@ #include #include "BKE_context.hh" +#include "BKE_lib_id.hh" #include "BKE_report.hh" #include "BKE_scene.hh" @@ -21,6 +22,7 @@ #include "DEG_depsgraph_query.hh" +#include "DNA_collection_types.h" #include "DNA_scene_types.h" #include "ED_object.hh" @@ -33,13 +35,23 @@ namespace blender::io::obj { -OBJDepsgraph::OBJDepsgraph(const bContext *C, const eEvaluationMode eval_mode) +OBJDepsgraph::OBJDepsgraph(const bContext *C, + const eEvaluationMode eval_mode, + Collection *collection) { Scene *scene = CTX_data_scene(C); Main *bmain = CTX_data_main(C); ViewLayer *view_layer = CTX_data_view_layer(C); - if (eval_mode == DAG_EVAL_RENDER) { - depsgraph_ = DEG_graph_new(bmain, scene, view_layer, DAG_EVAL_RENDER); + + /* If a collection was provided, use it. */ + if (collection) { + depsgraph_ = DEG_graph_new(bmain, scene, view_layer, eval_mode); + needs_free_ = true; + DEG_graph_build_from_collection(depsgraph_, collection); + BKE_scene_graph_evaluated_ensure(depsgraph_, bmain); + } + else if (eval_mode == DAG_EVAL_RENDER) { + depsgraph_ = DEG_graph_new(bmain, scene, view_layer, eval_mode); needs_free_ = true; DEG_graph_build_for_all_objects(depsgraph_); BKE_scene_graph_evaluated_ensure(depsgraph_, bmain); @@ -318,7 +330,22 @@ bool append_frame_to_filename(const char *filepath, const int frame, char *r_fil void exporter_main(bContext *C, const OBJExportParams &export_params) { ed::object::mode_set(C, OB_MODE_OBJECT); - OBJDepsgraph obj_depsgraph(C, export_params.export_eval_mode); + + Collection *collection = nullptr; + if (strlen(export_params.collection) > 0) { + Main *bmain = CTX_data_main(C); + collection = reinterpret_cast( + BKE_libblock_find_name(bmain, ID_GR, export_params.collection)); + if (!collection) { + BKE_reportf(export_params.reports, + RPT_ERROR, + "OBJ Export: Unable to find collection %s", + export_params.collection); + return; + } + } + + OBJDepsgraph obj_depsgraph(C, export_params.export_eval_mode, collection); Scene *scene = DEG_get_input_scene(obj_depsgraph.get()); const char *filepath = export_params.filepath; diff --git a/source/blender/io/wavefront_obj/exporter/obj_exporter.hh b/source/blender/io/wavefront_obj/exporter/obj_exporter.hh index 4180f29a254..768c3f1f716 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_exporter.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_exporter.hh @@ -14,6 +14,9 @@ #include "IO_wavefront_obj.hh" +struct bContext; +struct Collection; + namespace blender::io::obj { /** @@ -26,7 +29,7 @@ class OBJDepsgraph : NonMovable, NonCopyable { bool needs_free_ = false; public: - OBJDepsgraph(const bContext *C, eEvaluationMode eval_mode); + OBJDepsgraph(const bContext *C, eEvaluationMode eval_mode, Collection *collection); ~OBJDepsgraph(); Depsgraph *get(); diff --git a/source/blender/makesdna/DNA_collection_types.h b/source/blender/makesdna/DNA_collection_types.h index 41743d588b9..68c2cc91534 100644 --- a/source/blender/makesdna/DNA_collection_types.h +++ b/source/blender/makesdna/DNA_collection_types.h @@ -62,6 +62,23 @@ typedef struct CollectionChild { int _pad; } CollectionChild; +/* Collection IO property storage and access. */ +typedef struct CollectionExport { + struct CollectionExport *next, *prev; + + /** Identifier that matches the #FileHandlerType.idname. */ + char fh_idname[64]; + + IDProperty *export_properties; + uint32_t flag; + + uint32_t _pad0; +} CollectionExport; + +typedef enum IOHandlerPanelFlag { + IO_HANDLER_PANEL_OPEN = 1 << 0, +} IOHandlerPanelFlag; + /* Light linking state of object or collection: defines how they react to the emitters in the * scene. See the comment for the link_state in the CollectionLightLinking for the details. */ typedef enum eCollectionLightLinkingState { @@ -116,6 +133,8 @@ typedef struct Collection { /** CollectionChild. */ ListBase children; + ListBase exporters; + struct PreviewImage *preview; unsigned int layer DNA_DEPRECATED; diff --git a/source/blender/makesdna/DNA_layer_types.h b/source/blender/makesdna/DNA_layer_types.h index 661a7fde678..6d60be6af30 100644 --- a/source/blender/makesdna/DNA_layer_types.h +++ b/source/blender/makesdna/DNA_layer_types.h @@ -275,4 +275,5 @@ enum { /* VIEW_LAYER_DEPRECATED = (1 << 1), */ VIEW_LAYER_FREESTYLE = (1 << 2), VIEW_LAYER_OUT_OF_SYNC = (1 << 3), + VIEW_LAYER_HAS_EXPORT_COLLECTIONS = (1 << 4), }; diff --git a/source/blender/makesrna/intern/rna_collection.cc b/source/blender/makesrna/intern/rna_collection.cc index 6322e42e022..b745881cdb5 100644 --- a/source/blender/makesrna/intern/rna_collection.cc +++ b/source/blender/makesrna/intern/rna_collection.cc @@ -8,6 +8,8 @@ #include +#include "BKE_file_handler.hh" + #include "DNA_collection_types.h" #include "DNA_lineart_types.h" @@ -446,6 +448,25 @@ static void rna_CollectionLightLinking_update(Main *bmain, Scene * /*scene*/, Po DEG_relations_tag_update(bmain); } +static PointerRNA rna_CollectionExport_export_properties_get(PointerRNA *ptr) +{ + const CollectionExport *data = reinterpret_cast(ptr->data); + + /* If the File Handler or Operator is missing, we allow the data to be accessible + * as generic ID properties. */ + blender::bke::FileHandlerType *fh = blender::bke::file_handler_find(data->fh_idname); + if (!fh) { + return RNA_pointer_create(ptr->owner_id, &RNA_IDPropertyWrapPtr, data->export_properties); + } + + wmOperatorType *ot = WM_operatortype_find(fh->export_operator, false); + if (!ot) { + return RNA_pointer_create(ptr->owner_id, &RNA_IDPropertyWrapPtr, data->export_properties); + } + + return RNA_pointer_create(ptr->owner_id, ot->srna, data->export_properties); +} + #else /* collection.objects */ @@ -566,6 +587,29 @@ static void rna_def_collection_child(BlenderRNA *brna) prop, "Light Linking", "Light linking settings of the collection object"); } +static void rna_def_collection_exporter_data(BlenderRNA *brna) +{ + StructRNA *srna; + PropertyRNA *prop; + + srna = RNA_def_struct(brna, "CollectionExport", nullptr); + RNA_def_struct_sdna(srna, "CollectionExport"); + RNA_def_struct_ui_text(srna, "Collection Export Data", "Exporter configured for the collection"); + + prop = RNA_def_property(srna, "is_open", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_boolean_sdna(prop, nullptr, "flag", IO_HANDLER_PANEL_OPEN); + RNA_def_property_ui_text(prop, "Is Open", "Whether the panel is expanded or closed"); + RNA_def_property_flag(prop, PROP_NO_DEG_UPDATE); + RNA_def_property_update(prop, NC_SPACE | ND_SPACE_PROPERTIES, nullptr); + + prop = RNA_def_property(srna, "export_properties", PROP_POINTER, PROP_NONE); + RNA_def_property_struct_type(prop, "PropertyGroup"); + RNA_def_property_ui_text( + prop, "Export Properties", "Properties associated with the configured exporter"); + RNA_def_property_pointer_funcs( + prop, "rna_CollectionExport_export_properties_get", nullptr, nullptr, nullptr); +} + void RNA_def_collections(BlenderRNA *brna) { StructRNA *srna; @@ -651,6 +695,14 @@ void RNA_def_collections(BlenderRNA *brna) RNA_def_property_ui_text(prop, "Collection Children", "Children collections their parent-collection-specific settings"); + + /* Export Handlers. */ + prop = RNA_def_property(srna, "exporters", PROP_COLLECTION, PROP_NONE); + RNA_def_property_struct_type(prop, "CollectionExport"); + RNA_def_property_collection_sdna(prop, nullptr, "exporters", nullptr); + RNA_def_property_ui_text( + prop, "Collection Export Handlers", "Export Handlers configured for the collection"); + /* TODO(sergey): Functions to link and unlink collections. */ /* Flags */ @@ -753,6 +805,7 @@ void RNA_def_collections(BlenderRNA *brna) rna_def_collection_light_linking(brna); rna_def_collection_object(brna); rna_def_collection_child(brna); + rna_def_collection_exporter_data(brna); } #endif diff --git a/source/blender/makesrna/intern/rna_layer.cc b/source/blender/makesrna/intern/rna_layer.cc index b9548d61ee7..accd8f43a3d 100644 --- a/source/blender/makesrna/intern/rna_layer.cc +++ b/source/blender/makesrna/intern/rna_layer.cc @@ -659,6 +659,14 @@ void RNA_def_view_layer(BlenderRNA *brna) RNA_def_property_ui_text(prop, "Enabled", "Enable or disable rendering of this View Layer"); RNA_def_property_update(prop, NC_SCENE | ND_LAYER, nullptr); + /* Cached flag indicating if any Collection in this ViewLayer has an Exporter set. */ + prop = RNA_def_property(srna, "has_export_collections", PROP_BOOLEAN, PROP_NONE); + RNA_def_property_clear_flag(prop, PROP_EDITABLE); + RNA_def_property_boolean_sdna(prop, nullptr, "flag", VIEW_LAYER_HAS_EXPORT_COLLECTIONS); + RNA_def_property_ui_text(prop, + "Has export collections", + "At least one Collection in this View Layer has an exporter"); + prop = RNA_def_property(srna, "use_freestyle", PROP_BOOLEAN, PROP_NONE); RNA_def_property_boolean_sdna(prop, nullptr, "flag", VIEW_LAYER_FREESTYLE); RNA_def_property_ui_text(prop, "Freestyle", "Render stylized strokes in this Layer"); diff --git a/source/blender/makesrna/intern/rna_ui.cc b/source/blender/makesrna/intern/rna_ui.cc index e9bf59c62c7..b10048ec2aa 100644 --- a/source/blender/makesrna/intern/rna_ui.cc +++ b/source/blender/makesrna/intern/rna_ui.cc @@ -2392,7 +2392,14 @@ static void rna_def_file_handler(BlenderRNA *brna) RNA_def_property_ui_text( prop, "Operator", - "Operator that can handle import files with the extensions given in bl_file_extensions"); + "Operator that can handle import for files with the extensions given in bl_file_extensions"); + prop = RNA_def_property(srna, "bl_export_operator", PROP_STRING, PROP_NONE); + RNA_def_property_string_sdna(prop, nullptr, "type->export_operator"); + RNA_def_property_flag(prop, PROP_REGISTER_OPTIONAL); + RNA_def_property_ui_text( + prop, + "Operator", + "Operator that can handle export for files with the extensions given in bl_file_extensions"); prop = RNA_def_property(srna, "bl_label", PROP_STRING, PROP_NONE); RNA_def_property_string_sdna(prop, nullptr, "type->label"); diff --git a/source/blender/makesrna/intern/rna_ui_api.cc b/source/blender/makesrna/intern/rna_ui_api.cc index 27a8f56bbf7..63e16456c07 100644 --- a/source/blender/makesrna/intern/rna_ui_api.cc +++ b/source/blender/makesrna/intern/rna_ui_api.cc @@ -1699,6 +1699,10 @@ void RNA_api_ui_layout(StructRNA *srna) RNA_def_function_flag(func, FUNC_USE_CONTEXT); RNA_def_function_ui_description(func, "Generates the UI layout for the modifier stack"); + func = RNA_def_function(srna, "template_collection_exporters", "uiTemplateCollectionExporters"); + RNA_def_function_flag(func, FUNC_USE_CONTEXT); + RNA_def_function_ui_description(func, "Generates the UI layout for collection exporters"); + func = RNA_def_function(srna, "template_constraints", "uiTemplateConstraints"); RNA_def_function_flag(func, FUNC_USE_CONTEXT); RNA_def_function_ui_description(func, "Generates the panels for the constraint stack");