Skip to content

Commit df74d24

Browse files
committed
Code to import and test keyframe loop related code. Also code to split location and rotation into two seperate variables and a fit other bits.
1 parent bcca632 commit df74d24

6 files changed

Lines changed: 402 additions & 235 deletions

File tree

io_xplane2blender/importer/xplane_imp_cmd_builder.py

Lines changed: 225 additions & 116 deletions
Large diffs are not rendered by default.

io_xplane2blender/importer/xplane_imp_parser.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -174,12 +174,12 @@ def scan_float(s_itr:iter)
174174
name_hint = ""
175175

176176
elif directive == "ANIM_begin":
177-
builder.build_cmd("ANIM_begin")
177+
builder.build_cmd("ANIM_begin", name_hint=name_hint)
178178
elif directive == "ANIM_end":
179179
builder.build_cmd("ANIM_end")
180180
elif directive == "ANIM_trans_begin":
181181
dataref_path = components[0]
182-
builder.build_cmd("ANIM_trans_begin", dataref_path)
182+
builder.build_cmd("ANIM_trans_begin", dataref_path, name_hint=name_hint)
183183
elif directive == "ANIM_trans_key":
184184
value = float(components[0])
185185
location = vec_x_to_b(list(map(float, components[1:4])))
@@ -193,7 +193,7 @@ def scan_float(s_itr:iter)
193193
elif directive == "ANIM_rotate_begin":
194194
axis = vec_x_to_b(list(map(float, components[0:3])))
195195
dataref_path = components[3]
196-
builder.build_cmd(directive, axis, dataref_path)
196+
builder.build_cmd(directive, axis, dataref_path, name_hint=name_hint)
197197
elif directive == "ANIM_rotate_key":
198198
value = float(components[0])
199199
degrees = float(components[1])
@@ -215,7 +215,7 @@ def scan_float(s_itr:iter)
215215
path = components[8]
216216
except IndexError as e:
217217
pass
218-
builder.build_cmd(directive, xyz1, xyz2, v1, v2, path)
218+
builder.build_cmd(directive, xyz1, xyz2, v1, v2, path, name_hint=name_hint)
219219
elif directive == "ANIM_rotate":
220220
dxyz = vec_x_to_b(list(map(float, components[:3])))
221221
r1, r2 = map(float, components[3:5])
@@ -228,7 +228,7 @@ def scan_float(s_itr:iter)
228228
path = components[7]
229229
except IndexError:
230230
pass
231-
builder.build_cmd(directive, dxyz, r1, r2, v1, v2, path)
231+
builder.build_cmd(directive, dxyz, r1, r2, v1, v2, path, name_hint=name_hint)
232232
else:
233233
# print(f"{directive} is not implemted yet")
234234
pass

io_xplane2blender/tests/__init__.py

Lines changed: 93 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
ANIM_TYPE_HIDE,
2525
ANIM_TYPE_SHOW,
2626
ANIM_TYPE_TRANSFORM,
27+
PRECISION_KEYFRAME,
28+
PRECISION_OBJ_FLOAT,
2729
)
2830
from io_xplane2blender.xplane_helpers import XPlaneLogger, logger
2931
from 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,

io_xplane2blender/tests/test_creation_helpers.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ def __init__(
183183
dataref_value: Optional[float] = None,
184184
dataref_show_hide_v1: Optional[float] = None,
185185
dataref_show_hide_v2: Optional[float] = None,
186+
dataref_loop: Optional[float] = None,
186187
dataref_anim_type: str = xplane_constants.ANIM_TYPE_TRANSFORM, # Must be xplane_constants.ANIM_TYPE_*
187188
location: Optional[Vector] = None,
188189
rotation_mode: str = "XYZ",
@@ -199,6 +200,7 @@ def __init__(
199200
self.dataref_value = dataref_value
200201
self.dataref_show_hide_v1 = dataref_show_hide_v1
201202
self.dataref_show_hide_v2 = dataref_show_hide_v2
203+
self.dataref_loop = dataref_loop
202204
self.dataref_anim_type = dataref_anim_type
203205
self.location = location
204206
self.rotation_mode = rotation_mode
@@ -911,6 +913,8 @@ def set_animation_data(
911913
dataref_prop.show_hide_v2 = kf_info.dataref_show_hide_v2
912914
else:
913915
dataref_prop.value = kf_info.dataref_value
916+
# Multiple assignment isn't harmful
917+
dataref_prop.loop = kf_info.dataref_loop
914918

915919
if not kf_info.location and not kf_info.rotation:
916920
continue
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
I
2+
800
3+
OBJ
4+
5+
GLOBAL_cockpit_lit
6+
POINT_COUNTS 3 0 0 3
7+
8+
VT 1 0 1 -0 -1 -0 0.625 1 # 0
9+
VT 1 0 -1 -0 -1 -0 0.625 0.75 # 1
10+
VT -1 0 1 -0 -1 -0 0.375 1 # 2
11+
12+
IDX 0
13+
IDX 1
14+
IDX 2
15+
16+
# Unit Test Overview
17+
#
18+
# This tests that keyframe_loop is properly imported,
19+
# and incidentally that empty_2 is optimized out and
20+
# it is given to mesh_2.5.
21+
#
22+
# THe object names are the loop values
23+
24+
# 0 ROOT
25+
# 1 Mesh: Cube
26+
ANIM_begin
27+
# static rotation
28+
# translation keyframes
29+
# name_hint: empty_1.1
30+
ANIM_trans_begin dref_1
31+
ANIM_trans_key 0 0 0 -0
32+
ANIM_trans_key 1 2 0 -0
33+
ANIM_keyframe_loop 1.1
34+
ANIM_trans_end
35+
# translation keyframes
36+
# name_hint: empty_2
37+
ANIM_trans_begin dref_2
38+
ANIM_trans_key 0 0 0 -0
39+
ANIM_trans_key 1 2 0 -0
40+
ANIM_keyframe_loop 2.5
41+
ANIM_trans_end
42+
# name_hint: mesh_2.5
43+
ATTR_shiny_rat 0.5
44+
TRIS 0 3
45+
ANIM_end
46+
47+
# Build with Blender 2.80 (sub 75) (build b'f6cb5f54494e'). Exported with XPlane2Blender 4.1.0-beta.1+101.NO_BUILD_NUMBR

0 commit comments

Comments
 (0)