Skip to content

Commit

Permalink
Closes #570, new algorithm for filtering clamping keyframes and new n…
Browse files Browse the repository at this point in the history
…ames for these things
  • Loading branch information
tngreene committed Aug 30, 2020
1 parent 1611cbc commit e397e85
Show file tree
Hide file tree
Showing 4 changed files with 155 additions and 109 deletions.
169 changes: 112 additions & 57 deletions io_xplane2blender/xplane_types/xplane_keyframe_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from mathutils import Vector

from io_xplane2blender.xplane_types.xplane_keyframe import XPlaneKeyframe
from io_xplane2blender.xplane_helpers import round_vec
from io_xplane2blender import xplane_constants

# Class: XPlaneKeyframeCollection
#
Expand Down Expand Up @@ -138,51 +140,69 @@ def getDataref(self):
def getRotationMode(self):
return self._list[0].rotationMode

def getRotationKeyframeTable(self) -> List[Tuple[Vector, List["TableEntry"]]]:
AxisKeyframeTable = namedtuple('AxisKeyframeTable', ['axis', 'table'])
AxisKeyframeTable.__doc__ = "The reference axis and the table of keyframe rotations along it"
TableEntry = namedtuple('TableEntry', ['value','degrees'])
TableEntry.__doc__ = "An entry in the keyframe table, where value is the dataref value"

def getRotationKeyframeTables(self) -> List["XPlaneKeyframeCollection.AxisKeyframeTable"]:
'''
Return the rotation portion of a keyframe collection in the form of
List[Tuple[axis, List[Tuple[value,degrees]]]], where axis is Vector.
List of sub tables will either be 1 (for AA or Quaternion) or 3 (for Eulers)
'''
axes, final_rotation_mode = self.getReferenceAxes()
AxisKeyframeTable = XPlaneKeyframeCollection.AxisKeyframeTable
TableEntry = XPlaneKeyframeCollection.TableEntry

#List(length 1 or 3)[
# List[Vector:rotation axis, List[TableEntry]]
#]
ret = [[axis,None] for axis in axes]
TableEntry = namedtuple('TableEntry', ['value','degrees'])
if final_rotation_mode == "AXIS_ANGLE" or\
final_rotation_mode == "QUATERNION":
keyframe_table = [TableEntry(keyframe.dataref_value, math.degrees(keyframe.rotation[0])) for keyframe in self]
ret[0][1] = keyframe_table
axes, final_rotation_mode = self.getReferenceAxes()
if final_rotation_mode in {"AXIS_ANGLE", "QUATERNION"}:
rot_keyframe_tables = [
AxisKeyframeTable(
axis=axes[0],
table=[
TableEntry(
keyframe.dataref_value,
math.degrees(keyframe.rotation[0]),
)
for keyframe in self
],
)
]
else:
rot_keyframe_tables = []
cur_order = self.EULER_AXIS_ORDERING[final_rotation_mode]
ret[0][1] = [TableEntry(keyframe.dataref_value, math.degrees(keyframe.rotation[cur_order[0]])) for keyframe in self]
ret[1][1] = [TableEntry(keyframe.dataref_value, math.degrees(keyframe.rotation[cur_order[1]])) for keyframe in self]
ret[2][1] = [TableEntry(keyframe.dataref_value, math.degrees(keyframe.rotation[cur_order[2]])) for keyframe in self]

ret = [tuple((axis_info[0],axis_info[1])) for axis_info in ret]

assert isinstance(ret,list)
for axis_info in ret:
for i, axis in enumerate(axes):
rot_keyframe_tables.append(
AxisKeyframeTable(
axis=axis,
table=[
TableEntry(
keyframe.dataref_value,
math.degrees(keyframe.rotation[cur_order[i]]),
)
for keyframe in self
],
)
)

assert isinstance(rot_keyframe_tables,list)
for axis_info in rot_keyframe_tables:
assert isinstance(axis_info,tuple)
assert isinstance(axis_info[0],Vector)
assert isinstance(axis_info[1],list)
assert isinstance(axis_info.axis,Vector)
assert isinstance(axis_info.table,list)

for table_entry in axis_info[1]:
for table_entry in axis_info.table:
assert isinstance(table_entry,tuple)
assert isinstance(table_entry.value,float)
assert isinstance(table_entry.degrees,float)

return ret
return rot_keyframe_tables

def getRotationKeyframeTableNoClamps(self)->List[Tuple[Vector, List[Tuple['value','degrees']]]]:
def getRotationKeyframeTablesNoClamps(self)->List["XPlaneKeyframeCollection.AxisKeyframeTable"]:
'''
Return the rotation portion of a keyframe collection in the form of
List[Tuple[axis, List[Tuple[value,degrees]]]], where axis is Vector.
Does not contain any clamping keyframes.
Returns rotation keyframbe tables without clamping keyframes.
Throws a ValueError if all resulting keyframe tables would be less than 2 keyframes
(this should only be possible for certain Euler cases)
'''
return XPlaneKeyframeCollection.filter_clamping_keyframes(self.getRotationKeyframeTable(), "degrees")
return XPlaneKeyframeCollection.filter_clamping_keyframes(self.getRotationKeyframeTables(), "degrees")

def getTranslationKeyframeTable(self):
'''
Expand Down Expand Up @@ -226,11 +246,15 @@ def toQuaternion(self)->'XPlaneKeyframeCollection':
return self

@staticmethod
def filter_clamping_keyframes(keyframe_collection:'XPlaneKeyframeCollection',attr:str):
def filter_clamping_keyframes(keyframe_collection:'XPlaneKeyframeCollection',attr:str)->"XPlaneKeyframeCollection":
'''
Returns a new keyframe collection without clamping keyframes
attr specifies which keyframe attribute will be used to filter,
and must be "location" or "degrees"
and must be "location" or "degrees".
Raises ValueError if resulting cleaned XPlaneKeyframeCollection
would have less than 2 keyframes
(which should only be possible for Euler RotationKeyframeTables)
'''
assert attr in ("location","degrees")

Expand All @@ -239,31 +263,62 @@ def filter_clamping_keyframes(keyframe_collection:'XPlaneKeyframeCollection',att
# List[TranslationKeyframe[keyframe.value, keyframe.location]]
# elif attr == 'degrees
# List[RotationKeyframe['value','degrees']] from List[Tuple[axis, List[Tuple['value','degrees']]]]
def remove_clamp_keyframes(keyframes,attr):
itr = iter(keyframes)
while len(keyframes) > 2:
current = next(itr)
next_keyframe = next(itr,None)

if next_keyframe is not None:
if getattr(current,attr) == getattr(next_keyframe,attr):
del keyframes[keyframes.index(current)]
itr = iter(keyframes)
else:
break
ndigits = xplane_constants.PRECISION_KEYFRAME
def find_1st_non_clamping(keyframes, attr)->int:
def cmp_location(current, next_keyframe):
return round_vec(current.location, ndigits) != round_vec(
next_keyframe.location, ndigits
)

def cmp_rotation(current, next_keyframe):
return round(current.degrees, ndigits) != round(
next_keyframe.degrees, ndigits
)

if attr == "location":
cmp_fn = cmp_location
elif attr == "degrees":
cmp_fn = cmp_rotation

if len(keyframes) < 2:
raise ValueError("Keyframe table is less than 2 entries long")

for i in range(len(keyframes)-1):
if cmp_fn(keyframes[i], keyframes[i+1]):
break
else: # nobreak
raise ValueError("No non-clamping found")
return i

cleaned_keyframe_collection = keyframe_collection[:]
if attr == 'location':
remove_clamp_keyframes(cleaned_keyframe_collection,attr)
cleaned_keyframe_collection.reverse()
remove_clamp_keyframes(cleaned_keyframe_collection,attr)
cleaned_keyframe_collection.reverse()
return cleaned_keyframe_collection
try:
start = find_1st_non_clamping(keyframe_collection, attr)
except ValueError:
raise
else:
try:
end = len(keyframe_collection) - find_1st_non_clamping(list(reversed(keyframe_collection)), attr)
except ValueError:
raise
else:
return keyframe_collection[start: end]
elif attr == 'degrees':
for axis,table in cleaned_keyframe_collection:
remove_clamp_keyframes(table, attr)
table.reverse()
remove_clamp_keyframes(table, attr)
table.reverse()
return cleaned_keyframe_collection
new_keyframe_table = []
for i, (axis, table) in enumerate(keyframe_collection):
try:
start = find_1st_non_clamping(table, attr)
except ValueError:
new_keyframe_table.append(XPlaneKeyframeCollection.AxisKeyframeTable(axis, []))
continue
else:
try:
end = len(table) - find_1st_non_clamping(list(reversed(table)), attr)
except ValueError:
new_keyframe_table.append(XPlaneKeyframeCollection.AxisKeyframeTable(axis, []))
continue
else:
new_keyframe_table.append(XPlaneKeyframeCollection.AxisKeyframeTable(axis, table[start: end]))
if all(table == [] for axis, table in new_keyframe_table):
raise ValueError("XPlaneKeyframeCollection had only clamping keyframes")
return new_keyframe_table

69 changes: 17 additions & 52 deletions io_xplane2blender/xplane_types/xplane_manipulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ def check_bone_is_animated_on_n_axes(bone:XPlaneBone,num_axis_of_rotation:int, l
if log_errors:
assert manipulator

rotation_keyframe_table = next(iter(bone.animations.values())).getRotationKeyframeTable()
rotation_keyframe_table = next(iter(bone.animations.values())).getRotationKeyframeTables()

if len(rotation_keyframe_table) == 3:
deg_per_axis = []
Expand Down Expand Up @@ -250,7 +250,7 @@ def check_bones_rotation_translation_animations_are_orthogonal(rotation_bone:XPl
rotation_keyframe_table =\
next(iter(rotation_bone.animations.values()))\
.asAA()\
.getRotationKeyframeTable()
.getRotationKeyframeTables()

rotation_axis = rotation_keyframe_table[0][0]

Expand All @@ -268,43 +268,6 @@ def check_bones_rotation_translation_animations_are_orthogonal(rotation_bone:XPl
else:
return True

def _check_keyframe_rotation_count(rotation_bone:XPlaneBone, count:int, exclude_clamping:bool, cmp_func, cmp_error_msg:str, log_errors:bool=True,manipulator:'XPlaneManipulator'=None) -> bool:
if log_errors:
assert manipulator
keyframe_col = next(iter(rotation_bone.animations.values())).asAA()

if exclude_clamping:
res = cmp_func(len(keyframe_col.getRotationKeyframeTableNoClamps()[0]),count)
else:
res = cmp_func(len(keyframe_col.getRotationKeyframeTable()[0]),count)

if not res:
try:
logger.error("{} manipulator attached to {} must have {} {} {}keyframes for its rotation animation".format(
manipulator.manip.get_effective_type_name(),
rotation_bone.getBlenderName(),
cmp_error_msg,
count,
"non-clamping " if exclude_clamping else ""))
except:
logger.error("{} must have {} {} {}keyframes for its rotation animation".format(
rotation_bone.getBlenderName(),
cmp_error_msg,
count,
"non-clamping " if exclude_clamping else ""))

return False
else:
return True


def check_keyframe_rotation_eq_count(rotation_bone:XPlaneBone, count:int, exclude_clamping:bool, log_errors:bool=True,manipulator:'XPlaneManipulator'=None) -> bool:
return _check_keyframe_rotation_count(rotation_bone, count, exclude_clamping, lambda x,y: x==y, "exactly", log_errors,manipulator)


def check_keyframe_rotation_ge_count(rotation_bone:XPlaneBone, count:int, exclude_clamping:bool, log_errors:bool=True,manipulator:'XPlaneManipulator'=None) -> bool:
return _check_keyframe_rotation_count(rotation_bone, count, exclude_clamping, lambda x,y: x>=y, "greater than or equal to", log_errors,manipulator)

def _check_keyframe_translation_count(translation_bone:XPlaneBone, count:int, exclude_clamping:bool, cmp_func, cmp_error_msg:str, log_errors:bool=True,manipulator:'XPlaneManipulator'=None) -> bool:
if log_errors:
assert manipulator
Expand Down Expand Up @@ -352,7 +315,7 @@ def check_keyframes_rotation_are_orderered(rotation_bone:XPlaneBone, log_errors:
rotation_keyframe_table =\
next(iter(rotation_bone.animations.values()))\
.asAA()\
.getRotationKeyframeTable()
.getRotationKeyframeTables()

rotation_axis = rotation_keyframe_table[0][0]
rotation_keyframe_data = rotation_keyframe_table[0][1]
Expand Down Expand Up @@ -899,19 +862,21 @@ def collect(self)->None:
self.manip.dataref2 = "none"

rotation_origin = rotation_bone.getBlenderWorldMatrix().to_translation()
rotation_origin_xp = xplane_helpers.vec_b_to_x(rotation_origin)

rotation_keyframe_table_cleaned =\
next(iter(rotation_bone.animations.values()))\
.asAA()\
.getRotationKeyframeTableNoClamps()

rotation_axis = rotation_keyframe_table_cleaned[0][0]
kf_collection = next(iter(rotation_bone.animations.values()))

rotation_origin_xp = xplane_helpers.vec_b_to_x(rotation_origin)
rotation_axis_xp = xplane_helpers.vec_b_to_x(rotation_axis)
# If AA, we'll find the 1st table in a list of one
# if Euler we'll find the only table with entries
rotation_axis, rotation_table = next(
sub_table
for sub_table in kf_collection.getRotationKeyframeTablesNoClamps()
if sub_table.table
)
rotation_axis_xp = xplane_helpers.vec_b_to_x(rotation_axis)

v1_min, angle1 = rotation_keyframe_table_cleaned[0][1][0]
v1_max, angle2 = rotation_keyframe_table_cleaned[0][1][-1]
v1_min, angle1 = rotation_table[0]
v1_max, angle2 = rotation_table[-1]

if round(angle1,5) == round(angle2,5):
# Because of the previous guarantees that
Expand Down Expand Up @@ -1170,8 +1135,8 @@ def validate_axis_detent_ranges(axis_detent_ranges, translation_bone, v1_min, v1

# 4. All ATTR_manip_keyframes (DRAG_ROTATE)
if self.type == MANIP_DRAG_ROTATE or self.type == MANIP_DRAG_ROTATE_DETENT:
if len(rotation_keyframe_table_cleaned[0][1]) > 2:
for rot_keyframe in rotation_keyframe_table_cleaned[0][1][1:-1]:
if len(rotation_table) > 2:
for rot_keyframe in rotation_table[1:-1]:
self.xplanePrimative.cockpitAttributes.add(
XPlaneAttribute('ATTR_manip_keyframe', (rot_keyframe.value,rot_keyframe.degrees))
)
Expand Down
Binary file not shown.
26 changes: 26 additions & 0 deletions tests/object/drag_rotate_start_end_the_same.test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import inspect

from typing import Tuple
import os
import sys

import bpy
from io_xplane2blender import xplane_config
from io_xplane2blender.tests import *
from io_xplane2blender.tests import test_creation_helpers

__dirname__ = os.path.dirname(__file__)

class TestDragRotateStartEndTheSame(XPlaneTestCase):
def test_drag_rotate_start_end_the_same(self)->None:
filename = inspect.stack()[0].function

self.assertExportableRootExportEqualsFixture(
filename[5:],
os.path.join(__dirname__, "fixtures", f"{filename}.obj"),
{"ANIM", "ATTR_manip", "TRIS"},
filename,
)


runTestCases([TestDragRotateStartEndTheSame])

0 comments on commit e397e85

Please sign in to comment.