コード例 #1
0
    def write(self) -> str:
        """
        Writes collected Blender and XPlane data as a \\n
        seperated string of OBJ directives.
        """
        if self.export_animation_only:
            return ""

        debug = getDebug()
        o = ""

        xplaneFile = self.xplaneBone.xplaneFile
        commands = xplaneFile.commands

        if debug:
            indent = self.xplaneBone.getIndent()
            o += f"{indent}# {self.type}: {self.name}\tweight: {self.weight}\n"

        o += commands.writeReseters(self)

        for attr in self.attributes:
            o += commands.writeAttribute(self.attributes[attr], self)

        # if the file is a cockpit file write all cockpit attributes
        if xplaneFile.options.export_type == EXPORT_TYPE_COCKPIT:
            for attr in self.cockpitAttributes:
                o += commands.writeAttribute(self.cockpitAttributes[attr],
                                             self)

        return o
コード例 #2
0
ファイル: xplane_bone.py プロジェクト: xrotors/XPlane2Blender
    def _writeStaticTranslation(self, bakeMatrix):
        debug = getDebug()
        indent = self.getIndent()
        o = ''

        bakeMatrix = bakeMatrix

        translation = bakeMatrix.to_translation()
        translation[0] = round(translation[0],5)
        translation[1] = round(translation[1],5)
        translation[2] = round(translation[2],5)

        # ignore noop translations
        if translation[0] == 0 and translation[1] == 0 and translation[2] == 0:
            return o

        if debug:
            o += indent + '# static translation\n'

        o += indent + 'ANIM_trans\t%s\t%s\t%s\t%s\t%s\t%s\n' % (
            floatToStr(translation[0]),
            floatToStr(translation[2]),
            floatToStr(-translation[1]),
            floatToStr(translation[0]),
            floatToStr(translation[2]),
            floatToStr(-translation[1])
        )

        return o
コード例 #3
0
ファイル: xplane_bone.py プロジェクト: xrotors/XPlane2Blender
    def writeAnimationPrefix(self):
        debug = getDebug()
        indent = self.getIndent()
        o = ''

        if debug:
            o += indent + '# ' + self.getName() + '\n'
            '''
            if self.blenderBone:
                poseBone = self.blenderObject.pose.bones[self.blenderBone.name]
                if poseBone != None:
                    o += "# Armature\n" + str(self.blenderObject.matrix_world) + "\n"
                    if self.blenderBone.parent:
                        poseParent = self.blenderObject.pose.bones[self.blenderBone.parent.name]
                        if poseParent:
                            o += "#  parent matrix local rest\n" + str(self.blenderBone.parent.matrix_local) + "\n"
                            o += "#  parent matrix local pose\n" + str(poseParent.matrix) + "\n"
                            o += "#  delta r2r\n" + str(self.blenderBone.parent.matrix_local.inverted_safe() * self.blenderBone.matrix_local) + "\n"
                            o += "#  delta p2p\n" + str(poseParent.matrix.inverted_safe() * poseBone.matrix) + "\n"
                    o += "#   matrix local rest\n" + str(self.blenderBone.matrix_local) + "\n"
                    o += "#   matrix local pose\n" + str(poseBone.matrix) + "\n"
                    o += "#   pose delta\n" + str(self.blenderBone.matrix_local.inverted_safe() * poseBone.matrix) + "\n"
            elif self.blenderObject != None:
                o += "# Data block\n" + str(self.blenderObject.matrix_world) + "\n"

            # Debug code - this dumps the pre/post/bake matrix for every single xplane bone into the file.

            p = self
            while p != None:
               o += indent + '#   ' + p.getName() + '\n'
               o += str(p.getPreAnimationMatrix()) + '\n'
               o += str(p.getPostAnimationMatrix()) + '\n'
               o += str(p.getBakeMatrixForMyAnimations()) + '\n'
               p = None
            '''
        isAnimated = self.isAnimated()
        hasAnimationAttributes = (self.xplaneObject != None and len(self.xplaneObject.animAttributes) > 0)

        if not isAnimated and not hasAnimationAttributes:
            return o

        # and postMatrix is not preMatrix
        if (isAnimated) or \
            hasAnimationAttributes:
            o += indent + 'ANIM_begin\n'

        if isAnimated:# and postMatrix is not preMatrix:
            # write out static translations of bake
            bakeMatrix = self.getBakeMatrixForMyAnimations()
            o += self._writeStaticTranslation(bakeMatrix)
            o += self._writeStaticRotation(bakeMatrix)

            for dataref in sorted(list(self.animations.keys())):
                o += self._writeTranslationKeyframes(dataref)
            for dataref in sorted(list(self.animations.keys())):
                o += self._writeRotationKeyframes(dataref)

        o += self._writeAnimAttributes()

        return o
コード例 #4
0
ファイル: __init__.py プロジェクト: xrotors/XPlane2Blender
    def useLogger(self):
        debug = getDebug()
        logLevels = ['error', 'warning']

        if debug:
            logLevels.append('info')
            logLevels.append('success')

        logger.clear()
        logger.addTransport(XPlaneLogger.ConsoleTransport(), logLevels)
コード例 #5
0
ファイル: __init__.py プロジェクト: der-On/XPlane2Blender
    def useLogger(self):
        debug = getDebug()
        logLevels = ['error', 'warning']

        if debug:
            logLevels.append('info')
            logLevels.append('success')

        logger.clear()
        logger.addTransport(XPlaneLogger.ConsoleTransport(), logLevels)
コード例 #6
0
ファイル: xplane_bone.py プロジェクト: xrotors/XPlane2Blender
    def _writeStaticRotation(self, bakeMatrix):
        debug = getDebug()
        indent = self.getIndent()
        o = ''
        bakeMatrix = bakeMatrix
        rotation = bakeMatrix.to_euler('XYZ')
        rotation[0] = round(rotation[0],5)
        rotation[1] = round(rotation[1],5)
        rotation[2] = round(rotation[2],5)
        
        # ignore noop rotations
        if rotation[0] == 0 and rotation[1] == 0 and rotation[2] == 0:
            return o

        if debug:
            o += indent + '# static rotation\n'

		# Ben says: this is SLIGHTLY counter-intuitive...Blender axes are
		# globally applied in a Euler, so in our XYZ, X is affected -by- Y
		# and both are affected by Z.
		#
		# Since X-Plane works opposite this, we are going to apply the
		# animations exactly BACKWARD! ZYX.  The order here must
		# be opposite the decomposition order above.
		#
		# Note that since our axis naming is ALSO different this will
		# appear in the OBJ file as Y -Z X.
		#
		# see also: http://hacksoflife.blogspot.com/2015/11/blender-notepad-eulers.html

        axes = (2, 1, 0)
        eulerAxes = [(0.0,0.0,1.0),(0.0,1.0,0.0),(1.0,0.0,0.0)]
        i = 0

        for axis in eulerAxes:
            deg = math.degrees(rotation[axes[i]])

            # ignore zero rotation
            if not deg == 0:
                o += indent + 'ANIM_rotate\t%s\t%s\t%s\t%s\t%s\n' % (
                    floatToStr(axis[0]),
                    floatToStr(axis[2]),
                    floatToStr(-axis[1]),
                    floatToStr(deg), floatToStr(deg)
                )

            i += 1

        return o
コード例 #7
0
    def write(self):
        debug = xplane_config.getDebug()
        indent = self.xplaneBone.getIndent()
        o = super().write()

        special_empty_props = self.blenderObject.xplane.special_empty_props

        if (int(bpy.context.scene.xplane.version) >= 1130 and
            (special_empty_props.special_type == EMPTY_USAGE_EMITTER_PARTICLE
             or special_empty_props.special_type
             == EMPTY_USAGE_EMITTER_SOUND)):
            if not self.xplaneBone.xplaneFile.options.particle_system_file.endswith(
                    ".pss"):
                logger.error(
                    "Particle emitter {} is used, despite no .pss file being set"
                    .format(self.blenderObject.name))
                return ''
            elif special_empty_props.emitter_props.name.strip() == "":
                logger.error(
                    "Particle name for emitter {} can't be blank".format(
                        self.blenderObject.name))
                return ''

            bake_matrix = self.xplaneBone.getBakeMatrixForAttached()
            em_location = xplane_helpers.vec_b_to_x(
                bake_matrix.to_translation())
            #yaw,pitch,roll
            theta, psi, phi = tuple(
                map(math.degrees,
                    bake_matrix.to_euler()[:]))

            floatToStr = xplane_helpers.floatToStr
            o += '{indent}EMITTER {name} {x} {y} {z} {phi} {theta} {psi}'.format(
                indent=indent,
                name=special_empty_props.emitter_props.name,
                x=floatToStr(em_location.x),
                y=floatToStr(em_location.y),
                z=floatToStr(em_location.z),
                phi=floatToStr(-phi),  #yaw right
                theta=floatToStr(theta),  #pitch up
                psi=floatToStr(psi))  #roll right

            if (special_empty_props.emitter_props.index_enabled
                    and special_empty_props.emitter_props.index >= 0):
                o += ' {}'.format(special_empty_props.emitter_props.index)

            o += '\n'

        return o
コード例 #8
0
    def _writeStaticRotation(self, bakeMatrix: mathutils.Matrix) -> str:
        debug = getDebug()
        indent = self.getIndent()
        o = ''
        bakeMatrix = bakeMatrix
        rotation = list(
            map(lambda c: round(c, xplane_constants.PRECISION_KEYFRAME),
                bakeMatrix.to_euler('XYZ')))

        # ignore noop rotations
        if rotation == (0, 0, 0):
            return o

        if debug:
            o += indent + '# static rotation\n'

        # Ben says: this is SLIGHTLY counter-intuitive...Blender axes are
        # globally applied in a Euler, so in our XYZ, X is affected -by- Y
        # and both are affected by Z.
        #
        # Since X-Plane works opposite this, we are going to apply the
        # animations exactly BACKWARD! ZYX.  The order here must
        # be opposite the decomposition order above.
        #
        # Note that since our axis naming is ALSO different this will
        # appear in the OBJ file as Y -Z X.
        #
        # see also: http://hacksoflife.blogspot.com/2015/11/blender-notepad-eulers.html

        axes = (2, 1, 0)
        eulerAxes = [(0, 0, 1), (0, 1, 0), (1, 0, 0)]

        for i, axis in enumerate(eulerAxes):
            deg = math.degrees(rotation[axes[i]])

            # ignore zero rotation
            if not round(deg, xplane_constants.PRECISION_KEYFRAME) == 0:
                tab = "\t"
                o += (f"{indent}ANIM_rotate"
                      f"\t{tab.join(map(floatToStr,vec_b_to_x(axis)))}"
                      f"\t{tab.join(map(floatToStr, [deg, deg]))}\n")

        return o
コード例 #9
0
ファイル: xplane_bone.py プロジェクト: xrotors/XPlane2Blender
    def _writeEulerRotationKeyframes(self, dataref, keyframes):
        debug = getDebug()
        o = ''
        indent = self.getIndent()
        axes, final_rotation_mode = keyframes.getReferenceAxes()
        totalRot = 0

        for axis,order in zip(axes,XPlaneKeyframeCollection.EULER_AXIS_ORDERING[final_rotation_mode]):
            ao = ''
            totalAxisRot = 0

            ao += "%sANIM_rotate_begin\t%s\t%s\t%s\t%s\n" % (
                indent,
                floatToStr(axis[0]),
                floatToStr(axis[2]),
                floatToStr(-axis[1]),
                dataref
            )


            for keyframe in keyframes:
                deg = math.degrees(keyframe.rotation[order])
                totalRot += abs(deg)
                totalAxisRot += abs(deg)

                ao += "%sANIM_rotate_key\t%s\t%s\n" % (
                    indent,
                    floatToStr(keyframe.value),
                    floatToStr(deg)
                )

            ao += self._writeKeyframesLoop(dataref)
            ao += "%sANIM_rotate_end\n" % indent

            # do not write non-animated axis
            if round(totalAxisRot, FLOAT_PRECISION) > 0:
                o += ao

        # do not write zero rotations
        if round(totalRot, FLOAT_PRECISION) == 0:
            return ''

        return o
コード例 #10
0
ファイル: xplane_empty.py プロジェクト: der-On/XPlane2Blender
    def write(self):
        debug = xplane_config.getDebug()
        indent = self.xplaneBone.getIndent()
        o = super().write()

        special_empty_props = self.blenderObject.xplane.special_empty_props

        if (int(bpy.context.scene.xplane.version) >= 1130 and
                (special_empty_props.special_type == EMPTY_USAGE_EMITTER_PARTICLE or
                 special_empty_props.special_type == EMPTY_USAGE_EMITTER_SOUND)):
            if not self.xplaneBone.xplaneFile.options.particle_system_file.endswith(".pss"):
                logger.error("Particle emitter {} is used, despite no .pss file being set"
                             .format(self.blenderObject.name))
                return ''
            elif special_empty_props.emitter_props.name.strip() == "":
                logger.error("Particle name for emitter {} can't be blank"
                             .format(self.blenderObject.name))
                return ''

            bake_matrix = self.xplaneBone.getBakeMatrixForAttached()
            em_location = xplane_helpers.vec_b_to_x(bake_matrix.to_translation())
            #yaw,pitch,roll
            theta, psi, phi = tuple(map(math.degrees,bake_matrix.to_euler()[:]))

            floatToStr = xplane_helpers.floatToStr
            o += '{indent}EMITTER {name} {x} {y} {z} {phi} {theta} {psi}'.format(
                indent=indent,
                name=special_empty_props.emitter_props.name,
                x=floatToStr(em_location.x),
                y=floatToStr(em_location.y),
                z=floatToStr(em_location.z),
                phi=floatToStr(-phi), #yaw right
                theta=floatToStr(theta), #pitch up
                psi=floatToStr(psi)) #roll right

            if (special_empty_props.emitter_props.index_enabled and
                special_empty_props.emitter_props.index >= 0):
                o += ' {}'.format(special_empty_props.emitter_props.index)

            o += '\n'

        return o
コード例 #11
0
    def _writeRotationKeyframes(self, dataref) -> str:
        debug = getDebug()
        keyframes = self.animations[dataref]
        o = ''

        if not self.isDataRefAnimatedForRotation():
            return o

        if debug:
            o += f"{self.getIndent()}# rotation keyframes\n"

        rotationMode = keyframes[0].rotationMode

        if rotationMode == 'AXIS_ANGLE':
            o += self._writeAxisAngleRotationKeyframes(dataref, keyframes)
        elif rotationMode == 'QUATERNION':
            o += self._writeQuaternionRotationKeyframes(dataref, keyframes)
        else:
            o += self._writeEulerRotationKeyframes(dataref, keyframes)

        return o
コード例 #12
0
    def write(self):
        debug = getDebug()
        indent = self.xplaneBone.getIndent()
        o = ''

        xplaneFile = self.xplaneBone.xplaneFile
        commands =  xplaneFile.commands

        if debug:
            o += "%s# %s: %s\tweight: %d\n" % (indent, self.type, self.name, self.weight)

        o += commands.writeReseters(self)

        for attr in self.attributes:
            o += commands.writeAttribute(self.attributes[attr], self)

        # if the file is a cockpit file write all cockpit attributes
        if xplaneFile.options.export_type == EXPORT_TYPE_COCKPIT:
            for attr in self.cockpitAttributes:
                o += commands.writeAttribute(self.cockpitAttributes[attr], self)

        return o
コード例 #13
0
ファイル: xplane_bone.py プロジェクト: xrotors/XPlane2Blender
    def _writeTranslationKeyframes(self, dataref):
        debug = getDebug()
        keyframes = self.animations[dataref]
        
        o = ''
        
        if not self.isDataRefAnimatedForTranslation():
            return o
        
        # Apply scaling to translations
        pre_loc, pre_rot, pre_scale = self.getPreAnimationMatrix().decompose()
        
        totalTrans = 0
        indent = self.getIndent()

        if debug:
            o += indent + '# translation keyframes\n'

        o += "%sANIM_trans_begin\t%s\n" % (indent, dataref)

        for keyframe in keyframes:
            totalTrans += abs(keyframe.location[0]) + abs(keyframe.location[1]) + abs(keyframe.location[2])

            o += "%sANIM_trans_key\t%s\t%s\t%s\t%s\n" % (
                indent, floatToStr(keyframe.value),
                floatToStr(keyframe.location[0] * pre_scale[0]),
                floatToStr(keyframe.location[2] * pre_scale[2]),
                floatToStr(-keyframe.location[1] * pre_scale[1])
            )

        o += self._writeKeyframesLoop(dataref)
        o += "%sANIM_trans_end\n" % indent

        # do not write zero translations
        if totalTrans == 0:
            return ''

        return o
コード例 #14
0
    def _writeTranslationKeyframes(self, dataref: str) -> str:
        debug = getDebug()
        keyframes = self.animations[dataref]

        o = ''

        if not self.isDataRefAnimatedForTranslation():
            return o

        # Apply scaling to translations
        pre_loc, pre_rot, pre_scale = self.getPreAnimationMatrix().decompose()

        totalTrans = 0
        indent = self.getIndent()

        if debug:
            o += f"{indent}# translation keyframes\n"

        o += f"{indent}ANIM_trans_begin\t{dataref}\n"

        for keyframe in keyframes:
            totalTrans += sum(map(abs, keyframe.location))

            o += (f"{indent}ANIM_trans_key"
                  f"\t{floatToStr(keyframe.dataref_value)}"
                  f"\t{floatToStr(keyframe.location[0] * pre_scale[0])}"
                  f"\t{floatToStr(keyframe.location[2] * pre_scale[2])}"
                  f"\t{floatToStr(-keyframe.location[1] * pre_scale[1])}"
                  f"\n")

        o += self._writeKeyframesLoop(dataref)
        o += f"{indent}ANIM_trans_end\n"

        # do not write zero translations
        if totalTrans == 0:
            return ''

        return o
コード例 #15
0
ファイル: xplane_bone.py プロジェクト: der-On/XPlane2Blender
    def _writeRotationKeyframes(self, dataref):
        debug = getDebug()
        keyframes = self.animations[dataref]
        o = ''
       
        if not self.isDataRefAnimatedForRotation():
            return o
            
        indent = self.getIndent()

        if debug:
            o += indent + '# rotation keyframes\n'

        rotationMode = keyframes[0].rotationMode

        if rotationMode == 'AXIS_ANGLE':
            o += self._writeAxisAngleRotationKeyframes(dataref,keyframes)
        elif rotationMode == 'QUATERNION':
            o += self._writeQuaternionRotationKeyframes(dataref,keyframes)
        else:
            o += self._writeEulerRotationKeyframes(dataref,keyframes)

        return o
コード例 #16
0
    def _writeEulerRotationKeyframes(self, dataref, keyframes) -> str:
        debug = getDebug()
        o = ''
        indent = self.getIndent()
        axes, final_rotation_mode = keyframes.getReferenceAxes()
        totalRot = 0

        for axis, order in zip(
                axes, XPlaneKeyframeCollection.
                EULER_AXIS_ORDERING[final_rotation_mode]):
            ao = ''
            totalAxisRot = 0

            tab = "\t"
            ao += (f"{indent}ANIM_rotate_begin"
                   f"\t{tab.join(map(floatToStr, vec_b_to_x(axis)))}"
                   f"\t{dataref}\n")

            for keyframe in keyframes:
                deg = math.degrees(keyframe.rotation[order])
                totalRot += abs(deg)
                totalAxisRot += abs(deg)
                ao += f"{indent}ANIM_rotate_key\t{floatToStr(keyframe.dataref_value)}\t{floatToStr(deg)}\n"

            ao += self._writeKeyframesLoop(dataref)
            ao += f"{indent}ANIM_rotate_end\n"

            # do not write non-animated axis
            if round(totalAxisRot, xplane_constants.PRECISION_KEYFRAME) > 0:
                o += ao

        # do not write zero rotations
        if round(totalRot, xplane_constants.PRECISION_KEYFRAME) == 0:
            return ''

        return o
コード例 #17
0
    def collectAnimations(self) -> None:
        """
        Collects animation_data from blenderObject, and pairs it with xplane datarefs
        """
        if not self.parent:
            return None

        debug = getDebug()

        bone = self.blenderBone
        blenderObject = self.blenderObject

        #check for animation
        #if bone:
        #print("\t\t checking animations of %s:%s" % (blenderObject.name, bone.name))
        #else:
        #print("\t\t checking animations of %s" % blenderObject.name)

        try:
            if bone:
                # bone animation data resides in the armature objects .data block
                fcurves = [
                    f for f in blenderObject.data.animation_data.action.fcurves
                    if f.data_path.startswith(
                        f'bones["{bone.name}"].xplane.datarefs')
                ]
            else:
                fcurves = [
                    f for f in blenderObject.animation_data.action.fcurves
                    if f.data_path.startswith(f"xplane.datarefs")
                ]
        except AttributeError:
            pass
        else:
            for fcurve in fcurves:
                if bone:
                    index = int(
                        fcurve.
                        data_path[len(f'bones["{bone.name}"].xplane.datarefs['
                                      ):-len("].value")])
                else:
                    index = int(fcurve.data_path[len("xplane.datarefs["
                                                     ):-len("].value")])

                try:
                    if bone:
                        dataref = bone.xplane.datarefs[index].path
                    else:
                        dataref = blenderObject.xplane.datarefs[index].path
                except IndexError:
                    # Due to a long standing bug in (I think in BONE_OT_remove_xplane_dataref.execute)
                    # sometimes a Bone's fcurve is not properly removed. Any further indexes will also
                    # be wrong.
                    #
                    # TODO: Fix whatever is causing this, but, we'll still need this for old .blend files
                    # - Ted, 6/24/2020
                    return
                else:
                    if len(fcurve.keyframe_points) > 1:
                        if bone:
                            self.datarefs[dataref] = bone.xplane.datarefs[
                                index]
                        else:
                            self.datarefs[
                                dataref] = blenderObject.xplane.datarefs[index]

                        self.animations[dataref] = XPlaneKeyframeCollection([
                            XPlaneKeyframe(kf, i, dataref, self)
                            for i, kf in enumerate(fcurve.keyframe_points)
                        ])
コード例 #18
0
ファイル: xplane_bone.py プロジェクト: xrotors/XPlane2Blender
    def collectAnimations(self):
        if not self.parent:
            return None

        debug = getDebug()

        bone = self.blenderBone
        blenderObject = self.blenderObject

        # if bone:
        #     groupName = "XPlane Datarefs " + bone.name
        # else:
        #     groupName = "XPlane Datarefs"

        #check for animation
        if bone:
            logger.info("\t\t checking animations of %s:%s" % (blenderObject.name, bone.name))
        else:
            logger.info("\t\t checking animations of %s" % blenderObject.name)

        animationData = blenderObject.animation_data

        # bone animation data resides in the armature objects .data block
        if bone:
            animationData = blenderObject.data.animation_data

        if (animationData != None and animationData.action != None and len(animationData.action.fcurves) > 0):
            logger.info("\t\t animation found")
            #check for dataref animation by getting fcurves with the dataref group
            for fcurve in animationData.action.fcurves:
                logger.info("\t\t checking FCurve %s Group: %s" % (fcurve.data_path, fcurve.group))

                # Ben says: I'm not sure if this is the right way to do this -- when we iterate the fcurve data for this
                # armature, EVERY bone is included in a big pile.  So we parse the data_path and if it's clearly (1) for a bone and
                # (2) NOT for us, we skip it.  Without this, the key frames from differing bones get cross-contaminated in a multi-
                # bone case.
                if fcurve.data_path.startswith("bones[\"") and bone != None:
                    path_we_want = "bones[\"%s\"]" % bone.name
                    if not fcurve.data_path.startswith(path_we_want):
                        continue
                
                #if (fcurve.group != None and fcurve.group.name == groupName): # since 2.61 group names are not set so we have to check the datapath
                if ('xplane.datarefs' in fcurve.data_path):
                    # get dataref name
                    pos = fcurve.data_path.find('xplane.datarefs[')
                    if pos!=-1:
                        index = fcurve.data_path[pos+len('xplane.datarefs[') : -len('].value')]
                    else:
                        return

                    # old style datarefs with wrong datapaths can cause errors so we just skip them
                    try:
                        index = int(index)
                    except:
                        return

                    # FIXME: removed datarefs keep fcurves, so we have to check if dataref is still there.
                    # FCurves have to be deleted correctly.
                    if bone:
                        if index < len(bone.xplane.datarefs):
                            dataref = bone.xplane.datarefs[index].path
                        else:
                            return
                    else:
                        if index < len(blenderObject.xplane.datarefs):
                            dataref = blenderObject.xplane.datarefs[index].path
                        else:
                            return

                    logger.info("\t\t adding dataref animation: %s" % dataref)

                    if len(fcurve.keyframe_points) > 1:
                        # time to add dataref to animations

                        if bone:
                            self.datarefs[dataref] = bone.xplane.datarefs[index]
                        else:
                            self.datarefs[dataref] = blenderObject.xplane.datarefs[index]

                        # store keyframes temporary, so we can resort them
                        keyframes = []

                        for i,keyframe in enumerate(fcurve.keyframe_points):
                            logger.info("\t\t adding keyframe: %6.3f" % keyframe.co[0])
                            keyframes.append(XPlaneKeyframe(keyframe,i,dataref,self))

                        # sort keyframes by frame number
                        keyframesSorted = sorted(keyframes, key = lambda keyframe: keyframe.index)
                        self.animations[dataref] = XPlaneKeyframeCollection(keyframesSorted)