From 8fff063ff7abfee6fd917e2dc85e3d4f59032861 Mon Sep 17 00:00:00 2001 From: Thomas Barlow Date: Wed, 24 May 2023 05:22:26 +0100 Subject: [PATCH] Fix #104576: FBX export of Edit Mode Objects while Blender is not in Edit Mode can fail. All Objects to be exported are now checked for not being in Edit Mode prior to exporting. In the rare case that an Object is found to be in Edit Mode, it is temporarily added to the current view layer, temporarily set as active and then set to Object Mode. If an Object cannot be set to Object mode for some reason, an error is reported to the Operator and exporting is aborted. --- io_scene_fbx/export_fbx_bin.py | 20 ++++++++++++- io_scene_fbx/fbx_utils.py | 52 ++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index e5c724022..a58e7a5c1 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -77,7 +77,7 @@ from .fbx_utils import ( # Animation. AnimationCurveNodeWrapper, # Objects. - ObjectWrapper, fbx_name_class, + ObjectWrapper, fbx_name_class, ensure_object_not_in_edit_mode, # Top level. FBXExportSettingsMedia, FBXExportSettings, FBXExportData, ) @@ -3497,6 +3497,14 @@ def save(operator, context, ctx_objects = context.view_layer.objects if use_visible: ctx_objects = tuple(obj for obj in ctx_objects if obj.visible_get()) + + # Ensure no Objects are in Edit mode. + # Copy to a tuple for safety, to avoid the risk of modifying ctx_objects while iterating. + for obj in tuple(ctx_objects): + if not ensure_object_not_in_edit_mode(context, obj): + operator.report({'ERROR'}, "%s could not be set out of Edit Mode, so cannot be exported" % obj.name) + return {'CANCELLED'} + kwargs_mod["context_objects"] = ctx_objects depsgraph = context.evaluated_depsgraph_get() @@ -3528,6 +3536,16 @@ def save(operator, context, else: data_seq = tuple((scene, scene.name, 'objects') for scene in bpy.data.scenes if scene.objects) + # Ensure no Objects are in Edit mode. + for data, data_name, data_obj_propname in data_seq: + # Copy to a tuple for safety, to avoid the risk of modifying the data prop while iterating it. + for obj in tuple(getattr(data, data_obj_propname)): + if not ensure_object_not_in_edit_mode(context, obj): + operator.report({'ERROR'}, + "%s in %s could not be set out of Edit Mode, so cannot be exported" + % (obj.name, data_name)) + return {'CANCELLED'} + # call this function within a loop with BATCH_ENABLE == False new_fbxpath = fbxpath # own dir option modifies, we need to keep an original diff --git a/io_scene_fbx/fbx_utils.py b/io_scene_fbx/fbx_utils.py index 97cce3942..63ba317bf 100644 --- a/io_scene_fbx/fbx_utils.py +++ b/io_scene_fbx/fbx_utils.py @@ -540,6 +540,58 @@ def fast_first_axis_unique(ar, return_unique=True, return_index=False, return_in return result +def ensure_object_not_in_edit_mode(context, obj): + """Objects in Edit mode usually cannot be exported because much of the API used when exporting is not available for + Objects in Edit mode. + + Exiting the currently active Object (and any other Objects opened in multi-editing) from Edit mode is simple and + should be done with `bpy.ops.mesh.mode_set(mode='OBJECT')` instead of using this function. + + This function is for the rare case where an Object is in Edit mode, but the current context mode is not Edit mode. + This can occur from a state where the current context mode is Edit mode, but then the active Object of the current + View Layer is changed to a different Object that is not in Edit mode. This changes the current context mode, but + leaves the other Object(s) in Edit mode. + """ + if obj.mode != 'EDIT': + return True + + # Get the active View Layer. + view_layer = context.view_layer + + # A View Layer belongs to a scene. + scene = view_layer.id_data + + # Get the current active Object of this View Layer, so we can restore it once done. + orig_active = view_layer.objects.active + + # Check if obj is in the View Layer. If obj is not in the View Layer, it cannot be set as the active Object. + # We don't use `obj.name in view_layer.objects` because an Object from a Library could have the same name. + is_in_view_layer = any(o == obj for o in view_layer.objects) + + do_unlink_from_scene_collection = False + try: + if not is_in_view_layer: + # There might not be any enabled collections in the View Layer, so link obj into the Scene Collection + # instead, which is always available to all View Layers of that Scene. + scene.collection.objects.link(obj) + do_unlink_from_scene_collection = True + view_layer.objects.active = obj + + # Now we're finally ready to attempt to change obj's mode. + if bpy.ops.object.mode_set.poll(): + bpy.ops.object.mode_set(mode='OBJECT') + if obj.mode == 'EDIT': + # The Object could not be set out of EDIT mode and therefore cannot be exported. + return False + finally: + # Always restore the original active Object and unlink obj from the Scene Collection if it had to be linked. + view_layer.objects.active = orig_active + if do_unlink_from_scene_collection: + scene.collection.objects.unlink(obj) + + return True + + # ##### UIDs code. ##### # ID class (mere int). -- 2.30.2