# SPDX-License-Identifier: MIT OR GPL-3.0-or-later import traceback from collections.abc import Set as AbstractSet from pathlib import Path from typing import TYPE_CHECKING, Optional import bpy from bpy.app.translations import pgettext from bpy.props import BoolProperty, CollectionProperty, StringProperty from bpy.types import ( Armature, Context, Event, Object, Operator, Panel, SpaceFileBrowser, UILayout, ) from bpy_extras.io_utils import ExportHelper from ..common import ops, safe_removal, version from ..common.error_dialog import show_error_dialog from ..common.logger import get_logger from ..common.preferences import ( ExportPreferencesProtocol, copy_export_preferences, draw_export_preferences_layout, get_preferences, ) from ..common.workspace import save_workspace from ..editor import migration, search, validation from ..editor.extension import get_armature_extension from ..editor.ops import VRM_OT_open_url_in_web_browser, layout_operator from ..editor.property_group import CollectionPropertyProtocol, StringPropertyGroup from ..editor.validation import VrmValidationError from ..editor.vrm0.panel import ( draw_vrm0_humanoid_operators_layout, draw_vrm0_humanoid_optional_bones_layout, draw_vrm0_humanoid_required_bones_layout, ) from ..editor.vrm0.property_group import Vrm0HumanoidPropertyGroup from ..editor.vrm1.ops import VRM_OT_assign_vrm1_humanoid_human_bones_automatically from ..editor.vrm1.panel import ( draw_vrm1_humanoid_optional_bones_layout, draw_vrm1_humanoid_required_bones_layout, ) from ..editor.vrm1.property_group import Vrm1HumanBonesPropertyGroup from .abstract_base_vrm_exporter import AbstractBaseVrmExporter from .uni_vrm_vrm_animation_exporter import UniVrmVrmAnimationExporter from .vrm0_exporter import Vrm0Exporter from .vrm1_exporter import Vrm1Exporter logger = get_logger(__name__) def export_vrm_update_addon_preferences( export_op: "EXPORT_SCENE_OT_vrm", context: Context ) -> None: if export_op.use_addon_preferences: copy_export_preferences(source=export_op, destination=get_preferences(context)) validation.WM_OT_vrm_validator.detect_errors( context, export_op.errors, export_op.armature_object_name, ) class EXPORT_SCENE_OT_vrm(Operator, ExportHelper): bl_idname = "export_scene.vrm" bl_label = "Save" bl_description = "export VRM" bl_options: AbstractSet[str] = {"REGISTER"} filename_ext = ".vrm" filter_glob: StringProperty( # type: ignore[valid-type] default="*.vrm", options={"HIDDEN"}, ) use_addon_preferences: BoolProperty( # type: ignore[valid-type] name="Export using add-on preferences", description="Export using add-on preferences instead of operator arguments", ) export_invisibles: BoolProperty( # type: ignore[valid-type] name="Export Invisible Objects", update=export_vrm_update_addon_preferences, ) export_only_selections: BoolProperty( # type: ignore[valid-type] name="Export Only Selections", update=export_vrm_update_addon_preferences, ) enable_advanced_preferences: BoolProperty( # type: ignore[valid-type] name="Enable Advanced Options", update=export_vrm_update_addon_preferences, ) export_all_influences: BoolProperty( # type: ignore[valid-type] name="Export All Bone Influences", update=export_vrm_update_addon_preferences, ) export_lights: BoolProperty( # type: ignore[valid-type] name="Export Lights", update=export_vrm_update_addon_preferences, ) export_gltf_animations: BoolProperty( # type: ignore[valid-type] name="Export glTF Animations", update=export_vrm_update_addon_preferences, ) export_try_sparse_sk: BoolProperty( # type: ignore[valid-type] name="Use Sparse Accessors", update=export_vrm_update_addon_preferences, ) errors: CollectionProperty( # type: ignore[valid-type] type=validation.VrmValidationError, options={"HIDDEN"}, ) armature_object_name: StringProperty( # type: ignore[valid-type] options={"HIDDEN"}, ) ignore_warning: BoolProperty( # type: ignore[valid-type] options={"HIDDEN"}, ) def execute(self, context: Context) -> set[str]: try: if not self.filepath: return {"CANCELLED"} if self.use_addon_preferences: copy_export_preferences( source=get_preferences(context), destination=self ) return export_vrm( Path(self.filepath), self, context, armature_object_name=self.armature_object_name, ) except Exception: show_error_dialog( pgettext("Failed to export VRM."), traceback.format_exc(), ) raise def invoke(self, context: Context, event: Event) -> set[str]: self.use_addon_preferences = True copy_export_preferences(source=get_preferences(context), destination=self) if "gltf" not in dir(bpy.ops.export_scene): return ops.wm.vrm_gltf2_addon_disabled_warning( "INVOKE_DEFAULT", ) armatures, _ = collect_export_objects( context, self.armature_object_name, self, ) if len(armatures) > 1: return ops.wm.vrm_export_armature_selection("INVOKE_DEFAULT") if len(armatures) == 1: armature = armatures[0] armature_data = armature.data if not isinstance(armature_data, Armature): pass elif get_armature_extension(armature_data).is_vrm0(): Vrm0HumanoidPropertyGroup.fixup_human_bones(armature) Vrm0HumanoidPropertyGroup.update_all_node_candidates( context, armature_data.name ) humanoid = get_armature_extension(armature_data).vrm0.humanoid if all( b.node.bone_name not in b.node_candidates for b in humanoid.human_bones ): ops.vrm.assign_vrm0_humanoid_human_bones_automatically( armature_object_name=armature.name ) if not humanoid.all_required_bones_are_assigned(): return ops.wm.vrm_export_human_bones_assignment( "INVOKE_DEFAULT", armature_object_name=self.armature_object_name, ) elif get_armature_extension(armature_data).is_vrm1(): Vrm1HumanBonesPropertyGroup.fixup_human_bones(armature) Vrm1HumanBonesPropertyGroup.update_all_node_candidates( context, armature_data.name ) human_bones = get_armature_extension( armature_data ).vrm1.humanoid.human_bones if all( human_bone.node.bone_name not in human_bone.node_candidates for human_bone in ( human_bones.human_bone_name_to_human_bone().values() ) ): ops.vrm.assign_vrm1_humanoid_human_bones_automatically( armature_object_name=armature.name ) if ( not human_bones.all_required_bones_are_assigned() and not human_bones.allow_non_humanoid_rig ): return ops.wm.vrm_export_human_bones_assignment( "INVOKE_DEFAULT", armature_object_name=self.armature_object_name, ) if ops.vrm.model_validate( "INVOKE_DEFAULT", show_successful_message=False, armature_object_name=self.armature_object_name, ) != {"FINISHED"}: return {"CANCELLED"} validation.WM_OT_vrm_validator.detect_errors( context, self.errors, self.armature_object_name, ) if not self.ignore_warning and any( error.severity <= 1 for error in self.errors ): return ops.wm.vrm_export_confirmation( "INVOKE_DEFAULT", armature_object_name=self.armature_object_name ) return ExportHelper.invoke(self, context, event) def draw(self, _context: Context) -> None: pass # Is needed to get panels available if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. filter_glob: str # type: ignore[no-redef] use_addon_preferences: bool # type: ignore[no-redef] export_invisibles: bool # type: ignore[no-redef] export_only_selections: bool # type: ignore[no-redef] enable_advanced_preferences: bool # type: ignore[no-redef] export_all_influences: bool # type: ignore[no-redef] export_lights: bool # type: ignore[no-redef] export_gltf_animations: bool # type: ignore[no-redef] export_try_sparse_sk: bool # type: ignore[no-redef] errors: CollectionPropertyProtocol[ # type: ignore[no-redef] VrmValidationError ] armature_object_name: str # type: ignore[no-redef] ignore_warning: bool # type: ignore[no-redef] def export_vrm( filepath: Path, export_preferences: ExportPreferencesProtocol, context: Context, *, armature_object_name: str, ) -> set[str]: if ops.vrm.model_validate( "INVOKE_DEFAULT", show_successful_message=False, armature_object_name=armature_object_name, ) != {"FINISHED"}: return {"CANCELLED"} armature_objects, export_objects = collect_export_objects( context, armature_object_name, export_preferences, ) armature_object: Optional[Object] = next(iter(armature_objects), None) is_vrm1 = False with save_workspace( context, # Allow restoring after changing active object armature_object, ): if armature_object: armature_object_is_temporary = False armature_data = armature_object.data if isinstance(armature_data, Armature): is_vrm1 = get_armature_extension(armature_data).is_vrm1() else: armature_object_is_temporary = True ops.icyp.make_basic_armature("EXEC_DEFAULT") armature_object = context.view_layer.objects.active if not armature_object or armature_object.type != "ARMATURE": message = "Failed to generate temporary armature" raise RuntimeError(message) migration.migrate(context, armature_object.name) if is_vrm1: vrm_exporter: AbstractBaseVrmExporter = Vrm1Exporter( context, export_objects, armature_object, export_preferences, ) else: vrm_exporter = Vrm0Exporter( context, export_objects, armature_object, ) vrm_bytes = vrm_exporter.export_vrm() if vrm_bytes is None: return {"CANCELLED"} Path(filepath).write_bytes(vrm_bytes) if armature_object_is_temporary and not safe_removal.remove_object( context, armature_object ): logger.warning("Failed to remove temporary armature") return {"FINISHED"} class VRM_PT_export_file_browser_tool_props(Panel): bl_idname = "VRM_IMPORTER_PT_export_error_messages" bl_space_type = "FILE_BROWSER" bl_region_type = "TOOL_PROPS" bl_parent_id = "FILE_PT_operator" bl_label = "" bl_options: AbstractSet[str] = {"HIDE_HEADER"} @classmethod def poll(cls, context: Context) -> bool: space_data = context.space_data if not isinstance(space_data, SpaceFileBrowser): return False return space_data.active_operator.bl_idname == "EXPORT_SCENE_OT_vrm" def draw(self, context: Context) -> None: space_data = context.space_data if not isinstance(space_data, SpaceFileBrowser): return operator = space_data.active_operator if not isinstance(operator, EXPORT_SCENE_OT_vrm): return layout = self.layout warning_message = version.panel_warning_message() if warning_message: box = layout.box() warning_column = box.column(align=True) for index, warning_line in enumerate(warning_message.splitlines()): warning_column.label( text=warning_line, translate=False, icon="NONE" if index else "ERROR", ) show_vrm1_options = False armature_objects, _ = collect_export_objects( context, operator.armature_object_name, operator ) if ( armature_objects and (armature_object := armature_objects[0]) and (armature_data := armature_object.data) and isinstance(armature_data, Armature) ): show_vrm1_options = get_armature_extension(armature_data).is_vrm1() draw_export_preferences_layout( operator, layout, show_vrm1_options=show_vrm1_options, ) if operator.errors: validation.WM_OT_vrm_validator.draw_errors( layout.box(), operator.errors, show_successful_message=False, ) class VRM_PT_export_vrma_help(Panel): bl_idname = "VRM_PT_export_vrma_help" bl_space_type = "FILE_BROWSER" bl_region_type = "TOOL_PROPS" bl_parent_id = "FILE_PT_operator" bl_label = "" bl_options: AbstractSet[str] = {"HIDE_HEADER"} @classmethod def poll(cls, context: Context) -> bool: space_data = context.space_data if not isinstance(space_data, SpaceFileBrowser): return False return space_data.active_operator.bl_idname == "EXPORT_SCENE_OT_vrma" def draw(self, _context: Context) -> None: draw_help_message(self.layout) def menu_export(menu_op: Operator, _context: Context) -> None: vrm_export_op = layout_operator( menu_op.layout, EXPORT_SCENE_OT_vrm, text="VRM (.vrm)" ) vrm_export_op.use_addon_preferences = True vrm_export_op.armature_object_name = "" vrm_export_op.ignore_warning = False vrma_export_op = layout_operator( menu_op.layout, EXPORT_SCENE_OT_vrma, text="VRM Animation (.vrma)" ) vrma_export_op.armature_object_name = "" class EXPORT_SCENE_OT_vrma(Operator, ExportHelper): bl_idname = "export_scene.vrma" bl_label = "Save" bl_description = "Export VRM Animation" bl_options: AbstractSet[str] = {"REGISTER"} filename_ext = ".vrma" filter_glob: StringProperty( # type: ignore[valid-type] default="*.vrma", options={"HIDDEN"}, ) armature_object_name: StringProperty( # type: ignore[valid-type] options={"HIDDEN"}, ) def execute(self, context: Context) -> set[str]: try: if WM_OT_vrma_export_prerequisite.detect_errors( context, self.armature_object_name ): return {"CANCELLED"} if not self.filepath: return {"CANCELLED"} if not self.armature_object_name: armature = search.current_armature(context) else: armature = context.blend_data.objects.get(self.armature_object_name) if not armature: return {"CANCELLED"} return UniVrmVrmAnimationExporter.execute( context, Path(self.filepath), armature ) except Exception: show_error_dialog( pgettext("Failed to export VRM Animation."), traceback.format_exc(), ) raise def invoke(self, context: Context, event: Event) -> set[str]: if WM_OT_vrma_export_prerequisite.detect_errors( context, self.armature_object_name ): return ops.wm.vrma_export_prerequisite( "INVOKE_DEFAULT", armature_object_name=self.armature_object_name, ) return ExportHelper.invoke(self, context, event) def draw(self, _context: Context) -> None: pass # Is needed to get panels available if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. filter_glob: str # type: ignore[no-redef] armature_object_name: str # type: ignore[no-redef] class WM_OT_vrm_export_human_bones_assignment(Operator): bl_label = "Required VRM Human Bones Assignment" bl_idname = "wm.vrm_export_human_bones_assignment" bl_options: AbstractSet[str] = {"REGISTER"} armature_object_name: StringProperty( # type: ignore[valid-type] options={"HIDDEN"}, ) def execute(self, context: Context) -> set[str]: preferences = get_preferences(context) armatures, _ = collect_export_objects( context, self.armature_object_name, preferences, ) if len(armatures) != 1: return {"CANCELLED"} armature = armatures[0] armature_data = armature.data if not isinstance(armature_data, Armature): return {"CANCELLED"} if get_armature_extension(armature_data).is_vrm0(): Vrm0HumanoidPropertyGroup.fixup_human_bones(armature) Vrm0HumanoidPropertyGroup.update_all_node_candidates( context, armature_data.name ) humanoid = get_armature_extension(armature_data).vrm0.humanoid if not humanoid.all_required_bones_are_assigned(): return {"CANCELLED"} elif get_armature_extension(armature_data).is_vrm1(): Vrm1HumanBonesPropertyGroup.fixup_human_bones(armature) Vrm1HumanBonesPropertyGroup.update_all_node_candidates( context, armature_data.name ) human_bones = get_armature_extension( armature_data ).vrm1.humanoid.human_bones if ( not human_bones.all_required_bones_are_assigned() and not human_bones.allow_non_humanoid_rig ): return {"CANCELLED"} else: return {"CANCELLED"} return ops.export_scene.vrm( "INVOKE_DEFAULT", armature_object_name=self.armature_object_name ) def invoke(self, context: Context, _event: Event) -> set[str]: return context.window_manager.invoke_props_dialog(self, width=800) def draw(self, context: Context) -> None: preferences = get_preferences(context) armatures, _ = collect_export_objects( context, self.armature_object_name, preferences ) if not armatures: return armature = armatures[0] armature_data = armature.data if not isinstance(armature_data, Armature): return if get_armature_extension(armature_data).is_vrm0(): WM_OT_vrm_export_human_bones_assignment.draw_vrm0(self.layout, armature) elif get_armature_extension(armature_data).is_vrm1(): WM_OT_vrm_export_human_bones_assignment.draw_vrm1(self.layout, armature) @staticmethod def draw_vrm0(layout: UILayout, armature: Object) -> None: armature_data = armature.data if not isinstance(armature_data, Armature): return humanoid = get_armature_extension(armature_data).vrm0.humanoid if humanoid.all_required_bones_are_assigned(): alert_box = layout.box() alert_box.label( text="All Required VRM Human Bones have been assigned.", icon="CHECKMARK", ) else: alert_box = layout.box() alert_box.alert = True alert_box.label( text="There are unassigned Required VRM Human Bones." + " Please assign all.", icon="ERROR", ) draw_vrm0_humanoid_operators_layout(armature, layout) row = layout.split(factor=0.5) draw_vrm0_humanoid_required_bones_layout(armature, row.column()) draw_vrm0_humanoid_optional_bones_layout(armature, row.column()) @staticmethod def draw_vrm1(layout: UILayout, armature: Object) -> None: armature_data = armature.data if not isinstance(armature_data, Armature): return human_bones = get_armature_extension(armature_data).vrm1.humanoid.human_bones if human_bones.all_required_bones_are_assigned(): alert_box = layout.box() alert_box.label( text="All Required VRM Human Bones have been assigned.", icon="CHECKMARK", ) elif human_bones.allow_non_humanoid_rig: alert_box = layout.box() alert_box.label( text="This armature will be exported but not as a humanoid." + " It cannot have animations applied" + " for humanoid avatars.", icon="CHECKMARK", ) else: alert_box = layout.box() alert_box.alert = True alert_column = alert_box.column(align=True) for error_message in human_bones.error_messages(): alert_column.label(text=error_message, translate=False, icon="ERROR") layout_operator( layout, VRM_OT_assign_vrm1_humanoid_human_bones_automatically, icon="ARMATURE_DATA", ).armature_object_name = armature.name row = layout.split(factor=0.5) draw_vrm1_humanoid_required_bones_layout(armature, row.column()) draw_vrm1_humanoid_optional_bones_layout(armature, row.column()) non_humanoid_export_column = layout.column() non_humanoid_export_column.prop(human_bones, "allow_non_humanoid_rig") if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. armature_object_name: str # type: ignore[no-redef] class WM_OT_vrm_export_confirmation(Operator): bl_label = "VRM Export Confirmation" bl_idname = "wm.vrm_export_confirmation" bl_options: AbstractSet[str] = {"REGISTER"} errors: CollectionProperty(type=validation.VrmValidationError) # type: ignore[valid-type] armature_object_name: StringProperty( # type: ignore[valid-type] options={"HIDDEN"}, ) export_anyway: BoolProperty( # type: ignore[valid-type] name="Export Anyway", ) def execute(self, _context: Context) -> set[str]: if not self.export_anyway: return {"CANCELLED"} ops.export_scene.vrm( "INVOKE_DEFAULT", ignore_warning=True, armature_object_name=self.armature_object_name, ) return {"FINISHED"} def invoke(self, context: Context, _event: Event) -> set[str]: validation.WM_OT_vrm_validator.detect_errors( context, self.errors, self.armature_object_name, ) return context.window_manager.invoke_props_dialog(self, width=800) def draw(self, _context: Context) -> None: layout = self.layout layout.label( text="There is a high-impact warning. VRM may not export as intended.", icon="ERROR", ) column = layout.column() for error in self.errors: if error.severity != 1: continue column.prop( error, "message", text="", translate=False, ) layout.prop(self, "export_anyway") if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. errors: CollectionPropertyProtocol[ # type: ignore[no-redef] VrmValidationError ] armature_object_name: str # type: ignore[no-redef] export_anyway: bool # type: ignore[no-redef] class WM_OT_vrm_export_armature_selection(Operator): bl_label = "VRM Export Armature Selection" bl_idname = "wm.vrm_export_armature_selection" bl_options: AbstractSet[str] = {"REGISTER"} armature_object_name: StringProperty( # type: ignore[valid-type] options={"HIDDEN"}, ) armature_object_name_candidates: CollectionProperty( # type: ignore[valid-type] type=StringPropertyGroup, options={"HIDDEN"}, ) def execute(self, context: Context) -> set[str]: if not self.armature_object_name: return {"CANCELLED"} armature_object = context.blend_data.objects.get(self.armature_object_name) if not armature_object or armature_object.type != "ARMATURE": return {"CANCELLED"} ops.export_scene.vrm( "INVOKE_DEFAULT", armature_object_name=self.armature_object_name ) return {"FINISHED"} def invoke(self, context: Context, _event: Event) -> set[str]: if not self.armature_object_name: armature_object = search.current_armature(context) if armature_object: self.armature_object_name = armature_object.name self.armature_object_name_candidates.clear() for obj in context.blend_data.objects: if obj.type != "ARMATURE": continue candidate = self.armature_object_name_candidates.add() candidate.value = obj.name return context.window_manager.invoke_props_dialog(self, width=600) def draw(self, _context: Context) -> None: layout = self.layout layout.label( text="Multiple armatures were found; please select one to export as VRM.", icon="ERROR", ) layout.prop_search( self, "armature_object_name", self, "armature_object_name_candidates", icon="OUTLINER_OB_ARMATURE", text="", translate=False, ) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. armature_object_name: str # type: ignore[no-redef] armature_object_name_candidates: CollectionPropertyProtocol[ # type: ignore[no-redef] StringPropertyGroup ] class WM_OT_vrma_export_prerequisite(Operator): bl_label = "VRM Animation Export Prerequisite" bl_idname = "wm.vrma_export_prerequisite" bl_options: AbstractSet[str] = {"REGISTER"} armature_object_name: StringProperty( # type: ignore[valid-type] options={"HIDDEN"}, ) armature_object_name_candidates: CollectionProperty( # type: ignore[valid-type] type=StringPropertyGroup, options={"HIDDEN"}, ) @staticmethod def detect_errors(context: Context, armature_object_name: str) -> list[str]: error_messages: list[str] = [] if not armature_object_name: armature = search.current_armature(context) else: armature = context.blend_data.objects.get(armature_object_name) if not armature: error_messages.append(pgettext("Armature not found")) return error_messages armature_data = armature.data if not isinstance(armature_data, Armature): error_messages.append(pgettext("Armature not found")) return error_messages ext = get_armature_extension(armature_data) if get_armature_extension(armature_data).is_vrm1(): humanoid = ext.vrm1.humanoid if not humanoid.human_bones.all_required_bones_are_assigned(): error_messages.append(pgettext("Please assign required human bones")) else: error_messages.append(pgettext("Please set the version of VRM to 1.0")) return error_messages def execute(self, _context: Context) -> set[str]: return ops.export_scene.vrma( "INVOKE_DEFAULT", armature_object_name=self.armature_object_name ) def invoke(self, context: Context, _event: Event) -> set[str]: if not self.armature_object_name: armature_object = search.current_armature(context) if armature_object: self.armature_object_name = armature_object.name self.armature_object_name_candidates.clear() for obj in context.blend_data.objects: if obj.type != "ARMATURE": continue candidate = self.armature_object_name_candidates.add() candidate.value = obj.name return context.window_manager.invoke_props_dialog(self, width=800) def draw(self, context: Context) -> None: layout = self.layout layout.label( text="VRM Animation export requires a VRM 1.0 armature", icon="INFO", ) error_messages = WM_OT_vrma_export_prerequisite.detect_errors( context, self.armature_object_name ) layout.prop_search( self, "armature_object_name", self, "armature_object_name_candidates", icon="OUTLINER_OB_ARMATURE", text="Armature to be exported", ) if error_messages: error_column = layout.box().column(align=True) for error_message in error_messages: error_column.label(text=error_message, icon="ERROR", translate=False) if not self.armature_object_name: armature = search.current_armature(context) else: armature = context.blend_data.objects.get(self.armature_object_name) if armature: armature_data = armature.data if isinstance(armature_data, Armature): ext = get_armature_extension(armature_data) if get_armature_extension(armature_data).is_vrm1(): humanoid = ext.vrm1.humanoid if not humanoid.human_bones.all_required_bones_are_assigned(): WM_OT_vrm_export_human_bones_assignment.draw_vrm1( self.layout, armature ) draw_help_message(layout) if TYPE_CHECKING: # This code is auto generated. # To regenerate, run the `uv run tools/property_typing.py` command. armature_object_name: str # type: ignore[no-redef] armature_object_name_candidates: CollectionPropertyProtocol[ # type: ignore[no-redef] StringPropertyGroup ] def draw_help_message(layout: UILayout) -> None: help_message = pgettext( "Animations to be exported\n" + "- Humanoid bone rotations\n" + "- Humanoid hips bone translations\n" + "- Expression preview value\n" + "- Look At preview target translation\n" ) help_box = layout.box() help_column = help_box.column(align=True) for index, help_line in enumerate(help_message.splitlines()): help_column.label( text=help_line, translate=False, icon="NONE" if index else "INFO", ) open_op = layout_operator( help_column, VRM_OT_open_url_in_web_browser, icon="URL", text="Open help in a Web Browser", ) open_op.url = pgettext("https://vrm-addon-for-blender.info/en-us/animation/") def collect_export_objects( context: Context, armature_object_name: str, export_preferences: ExportPreferencesProtocol, ) -> tuple[list[Object], list[Object]]: export_objects = search.export_objects( context, armature_object_name, export_invisibles=export_preferences.export_invisibles, export_only_selections=export_preferences.export_only_selections, export_lights=export_preferences.enable_advanced_preferences and export_preferences.export_lights, ) armature_objects = [obj for obj in export_objects if obj.type == "ARMATURE"] return armature_objects, export_objects