2424 ANIM_TYPE_HIDE ,
2525 ANIM_TYPE_SHOW ,
2626 ANIM_TYPE_TRANSFORM ,
27+ PRECISION_KEYFRAME ,
28+ PRECISION_OBJ_FLOAT ,
2729)
2830from io_xplane2blender .xplane_helpers import XPlaneLogger , logger
2931from io_xplane2blender .xplane_types import (
@@ -130,42 +132,50 @@ def useLogger(self):
130132 logger .clear ()
131133 logger .addTransport (XPlaneLogger .ConsoleTransport (), logLevels )
132134
133- def assertAction (
135+
136+ def assertTransformAction (
134137 self ,
135138 bl_object : bpy .types .Object ,
136- expected_action_struct : xplane_imp_cmd_builder .IntermediateAnimation ,
139+ expected_inter_anim : xplane_imp_cmd_builder .IntermediateAnimation ,
137140 ):
138141 """
139142 Asserts that an object's action datablock matches the animation specified by the ImportedAnimation struct.
140- The action datablock must have 0 or 3 location/rotation_euler channels as is relavent.
141- There should be an even number of Blender and X-Plane keyframes.
142-
143- Every keyframe
144- #TODO: Stupid? Does the importer need to be very specific? Would we have to test that spec?
145- assertAnimation expects the imported and fixture Action information to test will be fixture and counts on the action have 0 or 3 channels exactly for testing location and rotation.
146- Those channels must have the same number of keyframes. Our importer will always create 3 channels with
147- an equal number of keyframes (even if some are just 0,0,0) for simplicity
143+
144+ Assumes
145+ - bl_object has an action with `location` or `rotation` (in XYZ Eulers), **exclusively**
146+ - Tests 1 dataref path at a time
148147 """
149148 try :
150149 action = bl_object .animation_data .action
151150 except AttributeError : # animation_data is None
152- pass
151+ assert False
153152 else :
154153
155- def action_as_intermediate_animation (bl_object : bpy .types .Object ):
156- real_action_struct = xplane_imp_cmd_builder .IntermediateAnimation (
157- [], collections .defaultdict (list ), []
154+ def action_as_intermediate_animation (
155+ bl_object : bpy .types .Object ,
156+ ) -> xplane_imp_cmd_builder .IntermediateAnimation :
157+ """
158+ Convert a bl_object's action into an IntermediateAnimation
159+ """
160+
161+ action_as_intermediate_animation = (
162+ xplane_imp_cmd_builder .IntermediateAnimation ()
158163 )
159164
160165 def recombine_fcurves (
161166 action : bpy .types .Action , data_path : str
162- ) -> Union [List [Vector ], Dict [Vector , float ]]:
167+ ) -> Union [List [Vector ], Euler ]:
168+ """
169+ Turns an FCurve's Location and Rotation into lists of Vectors or Eulers
170+
171+ TODO: Cannot handle missing channels or an object with location and rotation
172+ """
163173 try :
164- x_comp , y_comp , z_comp = [
174+ x_comp , y_comp , z_comp = (
165175 [k .co [1 ] for k in f .keyframe_points ]
166176 for f in action .fcurves
167177 if f .data_path == data_path
168- ]
178+ )
169179 except ValueError : # no matching fcurves found
170180 if data_path == "location" :
171181 return []
@@ -178,101 +188,93 @@ def recombine_fcurves(
178188 for x , y , z in zip (x_comp , y_comp , z_comp )
179189 ]
180190 elif data_path == "rotation_euler" :
181- return {
182- Vector ((1 , 0 , 0 )).freeze (): list (
183- map (math .degrees , x_comp )
184- ),
185- Vector ((0 , 1 , 0 )).freeze (): list (
186- map (math .degrees , y_comp )
187- ),
188- Vector ((0 , 0 , 1 )).freeze (): list (
189- map (math .degrees , z_comp )
190- ),
191- }
192-
193- if action :
194- real_action_struct .locations = recombine_fcurves (action , "location" )
195- real_action_struct .rotations = recombine_fcurves (
196- action , "rotation_euler"
197- )
191+ return Euler ((x_comp , y_comp , z_comp ))
192+
193+ action_as_intermediate_animation .locations = recombine_fcurves (
194+ bl_object .animation_data .action , "location"
195+ )
196+ action_as_intermediate_animation .rotations = recombine_fcurves (
197+ bl_object .animation_data .action , "rotation_euler"
198+ )
199+ # ---end recombine_fcurves-------------------------------------
198200
199201 def get_dataref_prop_from_data_path (
200- bl_object : bpy .types .Object , data_path : str
201- ) -> xplane_props .XPlaneDataref :
202- try :
203- return bl_object .xplane .datarefs [
204- int (
202+ bl_object : bpy .types .Object , expected_path : str
203+ ) -> Tuple [bpy .types .FCurve , xplane_props .XPlaneDataref ]:
204+ for fcurve in (
205+ fcurve
206+ for fcurve in bl_object .animation_data .action .fcurves
207+ if fcurve .data_path .startswith ("xplane.datarefs[" )
208+ ):
209+ try :
210+ data_path_idx = int (
205211 re .match (
206- "xplane\.datarefs\[(\d+)\]\.value" , data_path
212+ "xplane\.datarefs\[(\d+)\]\.value" , fcurve . data_path
207213 ).group (1 )
208214 )
209- ]
210- except AttributeError : # No match, None has no 'group'
211- pass # TODO: fail here? It is, after all, wrong to end up with the wrong number of datarefs
212- # Impossible do something, this is an error?
213- except IndexError :
214- pass # TODO: Do something
215-
216- data_paths_to_xp_values = {
217- f .data_path : [k .co [1 ] for k in f .keyframe_points ]
218- for f in action .fcurves
219- if f .data_path .startswith ("xplane.datarefs" )
220- }
221-
222- for dataref_prop , xp_values in [
223- (get_dataref_prop_from_data_path (bl_object , data_path ), xp_values )
224- for data_path , xp_values in data_paths_to_xp_values .items ()
225- ]:
226- real_action_struct .xp_dataref = (
227- xplane_imp_cmd_builder .IntermediateDataref (
228- values = xp_values ,
229- )
230- )
215+ except AttributeError :
216+ # We expect tests to be so simple that they only have 1 dataref,
217+ # but in case they have multiple...
218+ pass
219+ except IndexError : # Is this actually impossible?
220+ assert (
221+ False
222+ ), f"{ bl_object .name } has data_path as out of index: { expected_path } "
223+ else :
224+ # Unit tests are so carefuly authored
225+ # we don't need error handling here.
226+ # fcurves and datarefs will be 1:1
227+ dataref_prop = bl_object .xplane .datarefs [data_path_idx ]
228+ if dataref_prop .path == expected_path :
229+ return (fcurve , dataref_prop )
230+ else : # no early return
231+ assert (
232+ False
233+ ), f"{ expected_path } was not found in { bl_object .name } "
234+
235+ # ----end get_dataref_prop_from_data_path----------------------
236+
237+ expected_path = expected_inter_anim .xp_dataref .path
238+ xp_fcurve , dataref_prop = get_dataref_prop_from_data_path (
239+ bl_object , expected_path
240+ )
241+ xp_values : List [float ] = [k .co [1 ] for k in xp_fcurve .keyframe_points ]
242+ action_as_intermediate_animation .xp_dataref = xplane_imp_cmd_builder .IntermediateDataref (
243+ anim_type = dataref_prop .anim_type ,
244+ loop = dataref_prop .loop ,
245+ path = dataref_prop .path ,
246+ # Why? The importer tracks these seperately to make optimzations
247+ # XPlane2Blender will have the data on one "time line" only
248+ location_values = xp_values ,
249+ rotation_values = xp_values ,
250+ )
231251
232- return real_action_struct
252+ return action_as_intermediate_animation
233253
254+ # fmt: off
234255 real_action_struct = action_as_intermediate_animation (bl_object )
235-
236- def copy_rest_of_dref_prop (bl_object , real_action_struct ):
237- # In case there was no action and we'll only have show hides
238- if not real_action_struct .xp_dataref :
239- real_action_struct .xp_dataref = (
240- xplane_imp_parser .IntermediateDataref ()
241- )
242-
243- for bl_dref , real_xp_dataref in zip (
244- bl_object .xplane .datarefs , real_action_struct .xp_dataref
245- ):
246- real_xp_dataref .anim_type = bl_dref .anim_type
247- real_xp_dataref .loop = bl_dref .loop
248- real_xp_dataref .path = bl_dref .path
249-
250- real_xp_dataref .show_hide_v1 = bl_dref .show_hide_v1
251- real_xp_dataref .show_hide_v2 = bl_dref .show_hide_v2
252-
253- copy_rest_of_dref_prop (bl_object , real_action_struct )
256+ self .assertEqual (real_action_struct .xp_dataref .anim_type , expected_inter_anim .xp_dataref .anim_type )
257+ self .assertAlmostEqual (real_action_struct .xp_dataref .loop , expected_inter_anim .xp_dataref .loop , places = 1 )
258+ self .assertEqual (real_action_struct .xp_dataref .path , expected_inter_anim .xp_dataref .path )
259+ self .assertAlmostEqual (real_action_struct .xp_dataref .show_hide_v1 , expected_inter_anim .xp_dataref .show_hide_v1 , places = PRECISION_OBJ_FLOAT )
260+ self .assertAlmostEqual (real_action_struct .xp_dataref .show_hide_v2 , expected_inter_anim .xp_dataref .show_hide_v2 , places = PRECISION_OBJ_FLOAT )
261+ # fmt: on
254262
255263 for real_loc , expected_loc in zip (
256- real_action_struct .locations , expected_action_struct .locations
264+ real_action_struct .locations , expected_inter_anim .locations
257265 ):
258266 self .assertVectorAlmostEqual (real_loc , expected_loc , 1 )
259267
260268 for real_rot_degrees , expected_rot_degrees in [
261269 (
262270 real_action_struct .rotations [axis ],
263- expected_action_struct .rotations [axis ],
271+ expected_inter_anim .rotations [axis ],
264272 )
265273 for axis in real_action_struct .rotations
266274 ]:
267275 for real_deg , exp_deg in zip (real_rot_degrees , expected_rot_degrees ):
268276 self .assertAlmostEqual (real_deg , exp_deg , places = 1 )
269277
270- for (real_dataref_prop , expected_dataref_prop ) in zip (
271- sorted (real_action_struct .xp_dataref , key = lambda d : d .path ),
272- sorted (expected_action_struct .xp_datarefs , key = lambda d : d .path ),
273- ):
274- self .assertEqual (real_dataref_prop , expected_dataref_prop )
275-
276278 def assertImportSucceeds (self , filepath : Union [str , pathlib .Path ], msg : str = None ):
277279 """
278280 Tests import succeeds without syntatic or semantic errors.
@@ -292,9 +294,7 @@ def assertImportSucceeds(self, filepath: Union[str, pathlib.Path], msg: str = No
292294 try :
293295 result = xplane_imp_parser .import_obj (filepath )
294296 except xplane_imp_parser .UnrecoverableParserError :
295- self .fail (
296- msg = msg if msg else f"Import of { filepath } did not succeed" ,
297- )
297+ self .fail (msg = msg if msg else f"Import of { filepath } did not succeed" ,)
298298 else :
299299 self .assertEqual (
300300 result ,
0 commit comments