@@ -115,17 +115,53 @@ def create_export_copy(original_obj, context):
115
115
if not original_obj or original_obj .type != "MESH" :
116
116
raise ValueError ("Invalid object provided for copying." )
117
117
118
- try :
119
- # Direct API calls - no mode switching needed
120
- copy_obj = original_obj .copy ()
121
- copy_obj .data = original_obj .data .copy ()
122
- context .collection .objects .link (copy_obj )
123
- logger .info (f"Created copy: { copy_obj .name } from { original_obj .name } " )
124
- return copy_obj
125
- except Exception as e :
126
- raise RuntimeError (
127
- f"Failed to create copy of { original_obj .name } : { e } "
128
- ) from e
118
+ logger .info (f"Attempting to duplicate '{ original_obj .name } ' using operator..." )
119
+
120
+ # Use the context manager to handle selection and active object
121
+ with temp_selection_context (context ,
122
+ active_object = original_obj ,
123
+ selected_objects = [original_obj ]):
124
+ try :
125
+ # Duplicate the active object (linked)
126
+ # more direct and conventional way to perform a non-interactive
127
+ # linked duplication than bpy.ops.object.duplicate(linked=True)
128
+ bpy .ops .object .duplicate_move_linked (
129
+ OBJECT_OT_duplicate = {"linked" :True , "mode" :'TRANSLATION' },
130
+ TRANSFORM_OT_translate = {"value" :(0 , 0 , 0 )} # No actual move
131
+ )
132
+
133
+ # The new duplicate becomes the active object
134
+ # after the operator runs
135
+ copy_obj = context .view_layer .objects .active
136
+ logger .info (f"Successfully created duplicate "
137
+ f"'{ copy_obj .name } ' via operator." )
138
+
139
+ # Make Mesh Data Single User
140
+ if copy_obj and copy_obj .data and copy_obj .data .users > 1 :
141
+ logger .info (f"Making mesh data single user for "
142
+ f"'{ copy_obj .name } '" )
143
+ copy_obj .data = copy_obj .data .copy ()
144
+
145
+ return copy_obj
146
+
147
+ except Exception as e :
148
+ logger .error (f"Error using duplicate_move_linked operator for "
149
+ f"'{ original_obj .name } ': { e } " , exc_info = True )
150
+ # Attempt cleanup if copy_obj was created but failed later
151
+ copy_obj_ref = None
152
+ try :
153
+ copy_obj_ref = context .view_layer .objects .active
154
+ if copy_obj_ref and copy_obj_ref != original_obj :
155
+ logger .info (f"Attempting cleanup of partially created "
156
+ f"copy: { copy_obj_ref .name } " )
157
+ bpy .data .objects .remove (copy_obj_ref , do_unlink = True )
158
+ except Exception as cleanup_e :
159
+ logger .warning (f"Issue during cleanup after copy "
160
+ f"failure: { cleanup_e } " )
161
+
162
+ raise RuntimeError (
163
+ f"Failed to create operator copy of { original_obj .name } : { e } "
164
+ ) from e
129
165
130
166
131
167
def sanitise_filename (name ):
@@ -182,10 +218,48 @@ def setup_export_object(obj, original_obj_name, scene_props, lod_level=None):
182
218
obj .name = final_name
183
219
logger .info (f"Renamed to: { obj .name } " )
184
220
221
+ # Check if object's scale is already 1.0
222
+ if obj .scale != (1.0 , 1.0 , 1.0 ):
223
+ # Apply scale transform if not 1.0
224
+ logger .info (f"Object scale is not 1.0: { obj .scale } , applying..." )
225
+ with bpy .context .temp_override (
226
+ selected_editable_objects = [obj ],
227
+ selected_objects = [obj ], active_object = obj ):
228
+ bpy .ops .object .transform_apply (location = False ,
229
+ rotation = False ,
230
+ scale = True )
231
+
232
+ # Zero location if specified in scene properties
185
233
if scene_props .mesh_export_zero_location :
186
234
obj .location = (0.0 , 0.0 , 0.0 )
187
235
logger .info (f"Zeroed location for { obj .name } " )
188
236
237
+ # Calculate final scale factor
238
+ final_scale_factor = scene_props .mesh_export_scale
239
+ if scene_props .mesh_export_units == "CENTIMETERS" :
240
+ # Apply 100x scale for Meters (Blender default)
241
+ # to Centimeters (UE default)
242
+ final_scale_factor *= 100.0
243
+ logger .info ("Applying M to CM scale factor (x100)" )
244
+
245
+ # Set the object's scale
246
+ if abs (final_scale_factor - 1.0 ) > 1e-6 : # Check if scaling is needed
247
+ obj .scale = (final_scale_factor ,
248
+ final_scale_factor ,
249
+ final_scale_factor )
250
+ logger .info (f"Set object scale to { final_scale_factor :.2f} " )
251
+ else :
252
+ logger .info (f"Final scale factor is 1.0. No scaling needed." )
253
+
254
+ # Apply the calculated scale transform using temp_override
255
+ with bpy .context .temp_override (
256
+ selected_editable_objects = [obj ],
257
+ selected_objects = [obj ], active_object = obj ):
258
+ bpy .ops .object .transform_apply (location = False ,
259
+ rotation = True ,
260
+ scale = True )
261
+ logger .info (f"Applied final scale transform for { obj .name } " )
262
+
189
263
return obj .name , base_name
190
264
except Exception as e :
191
265
raise RuntimeError (
@@ -428,7 +502,8 @@ def restore_original_textures(obj):
428
502
bpy .data .images .remove (img )
429
503
430
504
431
- def apply_decimate_modifier (obj , ratio , decimate_type , sym_axis = "X" , sym = False ):
505
+ def apply_decimate_modifier (obj , ratio , decimate_type ,
506
+ sym_axis = "X" , sym = False ):
432
507
"""
433
508
Adds, configures, and applies a Decimate modifier.
434
509
@@ -476,7 +551,8 @@ def apply_decimate_modifier(obj, ratio, decimate_type, sym_axis="X", sym=False):
476
551
# Ensure axis is valid before setting
477
552
axis_upper = sym_axis .upper ()
478
553
if axis_upper not in {'X' , 'Y' , 'Z' }:
479
- logger .error (f"Invalid symmetry axis value received: { sym_axis } . Aborting modifier application." )
554
+ logger .error (f"Invalid symmetry axis value received: "
555
+ f"{ sym_axis } . Aborting modifier application." )
480
556
obj .modifiers .remove (dec_mod ) # Clean up modifier
481
557
raise ValueError (f"Invalid symmetry axis: { sym_axis } " )
482
558
@@ -604,7 +680,7 @@ def export_object(obj, file_path, scene_props):
604
680
# logger.info(f"[[quality: {export_quality}]]")
605
681
# logger.info(f"[[downscale: {downscale_size}]]")
606
682
607
- logger .info (f"File path: { file_path } " )
683
+ # logger.info(f"File path: {file_path}")
608
684
logger .info (
609
685
f"Exporting { os .path .basename (export_filepath )} ({ fmt } )..."
610
686
)
@@ -616,9 +692,10 @@ def export_object(obj, file_path, scene_props):
616
692
bpy .ops .export_scene .fbx (
617
693
filepath = export_filepath ,
618
694
use_selection = True ,
619
- global_scale = scene_props . mesh_export_scale ,
695
+ global_scale = 1.0 , # Scale applied setup_export_object
620
696
axis_forward = scene_props .mesh_export_coord_forward ,
621
697
axis_up = scene_props .mesh_export_coord_up ,
698
+ apply_unit_scale = False ,
622
699
apply_scale_options = "FBX_SCALE_ALL" ,
623
700
object_types = {"MESH" },
624
701
path_mode = "COPY" ,
@@ -630,7 +707,7 @@ def export_object(obj, file_path, scene_props):
630
707
bpy .ops .wm .obj_export (
631
708
filepath = export_filepath ,
632
709
export_selected_objects = True ,
633
- global_scale = scene_props . mesh_export_scale ,
710
+ global_scale = 1.0 , # Scale applied setup_export_object
634
711
forward_axis = scene_props .mesh_export_coord_forward ,
635
712
up_axis = scene_props .mesh_export_coord_up ,
636
713
export_materials = True ,
@@ -675,7 +752,7 @@ def export_object(obj, file_path, scene_props):
675
752
bpy .ops .wm .stl_export (
676
753
filepath = export_filepath ,
677
754
export_selected_objects = True ,
678
- global_scale = scene_props . mesh_export_scale ,
755
+ global_scale = 1.0 , # Scale applied setup_export_object
679
756
forward_axis = scene_props .mesh_export_coord_forward ,
680
757
up_axis = scene_props .mesh_export_coord_up ,
681
758
apply_modifiers = False , # Handled by apply_mesh_modifiers
0 commit comments