diff --git a/io_export_after_effects.py b/io_export_after_effects.py
index 8d73d1936c2a8c50991154770ef490f42f6a1566..4861de9e1367d6eb74e1dd1cb8598b2b65447df1 100644
--- a/io_export_after_effects.py
+++ b/io_export_after_effects.py
@@ -22,8 +22,8 @@ bl_info = {
     "name": "Export: Adobe After Effects (.jsx)",
     "description": "Export cameras, selected objects & camera solution "
         "3D Markers to Adobe After Effects CS3 and above",
-    "author": "Bartek Skorupa",
-    "version": (0, 0, 70),
+    "author": "Bartek Skorupa, Damien Picard (@pioverfour)",
+    "version": (0, 1, 0),
     "blender": (2, 80, 0),
     "location": "File > Export > Adobe After Effects (.jsx)",
     "warning": "",
@@ -61,7 +61,7 @@ def get_comp_data(context):
         'end': end,
         'duration': (end - start + 1.0) / fps,
         'active_cam_frames': active_cam_frames,
-        'curframe': scene.frame_current,
+        'frame_current': scene.frame_current,
         }
 
 
@@ -97,40 +97,285 @@ def get_active_cam_for_each_frame(scene, start, end):
     return(active_cam_frames)
 
 
-def get_selected(context):
-    """Create manageable list of selected objects"""
-    cameras = []  # List of selected cameras
-    solids = []   # List of selected meshes exported as AE solids
-    images = []   # List of selected meshes exported as AE AV layers
-    lights = []   # List of selected lights exported as AE lights
-    nulls = []    # List of selected objects except cameras (will be used to create nulls in AE)
-    obs = context.selected_objects
+class ObjectExport():
+    """Base exporter class
 
-    for ob in obs:
-        if ob.type == 'CAMERA':
-            cameras.append(ob)
+    Collects data about an object and outputs the proper JSX script for AE.
+    """
+    def __init__(self, obj):
+        self.obj = obj
+        self.name_ae = convert_name(self.obj.name)
+        self.keyframes = {}
+
+    def get_prop_keyframe(self, context, prop_name, value, time):
+        """Set keyframe for given property"""
+        prop_keys = self.keyframes.setdefault(prop_name, [])
+        if not len(prop_keys) or value != prop_keys[-1][1]:
+            prop_keys.append((time, value))
+
+    def get_keyframe(self, context, data, time, ae_size):
+        """Store animation for the current frame"""
+        ae_transform = convert_transform_matrix(self.obj.matrix_world,
+                                                data['width'], data['height'],
+                                                data['aspect'], True, ae_size)
+
+        self.get_prop_keyframe(context, 'position', ae_transform[0:3], time)
+        self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time)
+        self.get_prop_keyframe(context, 'scale', ae_transform[6:9], time)
+
+    def get_obj_script(self, include_animation):
+        """Get the JSX script for the object"""
+        return self.get_type_script() + self.get_prop_script(include_animation) + self.get_post_script()
+
+    def get_type_script(self):
+        """Get the basic part of the JSX script"""
+        type_script = f'var {self.name_ae} = newComp.layers.addNull();\n'
+        type_script += f'{self.name_ae}.threeDLayer = true;\n'
+        type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n'
+        return type_script
+
+    def get_prop_script(self, include_animation):
+        """Get the part of the JSX script encoding animation"""
+        prop_script = ""
 
-        elif is_image_plane(ob):
-            images.append(ob)
+        # Set values of properties, add keyframes only where needed
+        for prop, keys in self.keyframes.items():
+            if include_animation and len(keys) > 1:
+                times = ",".join((str(k[0]) for k in keys))
+                values = ",".join((str(k[1]) for k in keys)).replace(" ", "")
+                prop_script += (
+                    f'{self.name_ae}.property("{prop}").setValuesAtTimes([{times}],[{values}]);\n')
+            else:
+                value = str(keys[0][1]).replace(" ", "")
+                prop_script += (
+                    f'{self.name_ae}.property("{prop}").setValue({value});\n')
+        prop_script += '\n'
+
+        return prop_script
+
+    def get_post_script(self):
+        """This is only used in lights as a post-treatment after animation"""
+        return ""
+
+class CameraExport(ObjectExport):
+    def get_keyframe(self, context, data, time, ae_size):
+        ae_transform = convert_transform_matrix(self.obj.matrix_world,
+                                                data['width'], data['height'],
+                                                data['aspect'], True, ae_size)
+        zoom = convert_lens(self.obj, data['width'], data['height'],
+                            data['aspect'])
+
+        self.get_prop_keyframe(context, 'position', ae_transform[0:3], time)
+        self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time)
+        self.get_prop_keyframe(context, 'zoom', zoom, time)
+
+    def get_type_script(self):
+        type_script = f'var {self.name_ae} = newComp.layers.addCamera("{self.name_ae}",[0,0]);\n'
+        type_script += f'{self.name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n'
+        return type_script
+
+
+class LightExport(ObjectExport):
+    def get_keyframe(self, context, data, time, ae_size):
+        ae_transform = convert_transform_matrix(self.obj.matrix_world,
+                                                data['width'], data['height'],
+                                                data['aspect'], True, ae_size)
+        self.type = self.obj.data.type
+        color = list(self.obj.data.color)
+        intensity = self.obj.data.energy * 10.0
+
+        self.get_prop_keyframe(context, 'position', ae_transform[0:3], time)
+        if self.type in {'SPOT', 'SUN'}:
+            self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time)
+        self.get_prop_keyframe(context, 'intensity', intensity, time)
+        self.get_prop_keyframe(context, 'Color', color, time)
+        if self.type == 'SPOT':
+            cone_angle = degrees(self.obj.data.spot_size)
+            self.get_prop_keyframe(context, 'Cone Angle', cone_angle, time)
+            cone_feather = self.obj.data.spot_blend * 100.0
+            self.get_prop_keyframe(context, 'Cone Feather', cone_feather, time)
+
+    def get_type_script(self):
+        type_script = f'var {self.name_ae} = newComp.layers.addLight("{self.name_ae}", [0.0, 0.0]);\n'
+        type_script += f'{self.name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n'
+        type_script += f'{self.name_ae}.lightType = LightType.SPOT;\n'
+        return type_script
+
+    def get_post_script(self):
+        """Set light type _after_ the orientation, otherwise the property is hidden in AE..."""
+        if self.obj.data.type == 'SUN':
+            post_script = f'{self.name_ae}.lightType = LightType.PARALLEL;\n'
+        elif self.obj.data.type == 'SPOT':
+            post_script = f'{self.name_ae}.lightType = LightType.SPOT;\n'
+        else:
+            post_script = f'{self.name_ae}.lightType = LightType.POINT;\n'
+        return post_script
+
+
+class ImageExport(ObjectExport):
+    def get_keyframe(self, context, data, time, ae_size):
+        # Convert obj transform properties to AE space
+        plane_matrix = get_image_plane_matrix(self.obj)
+        # Scale plane to account for AE's transforms
+        plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4)
+
+        ae_transform = convert_transform_matrix(plane_matrix, data['width'],
+                                                data['height'], data['aspect'],
+                                                True, ae_size)
+        opacity = 0.0 if self.obj.hide_render else 100.0
+
+        if not hasattr(self, 'filepath'):
+            self.filepath = get_image_filepath(self.obj)
+
+        image_width, image_height = get_image_size(self.obj)
+        ratio_to_comp = image_width / data['width']
+        scale = ae_transform[6:9]
+        scale[0] /= ratio_to_comp
+        scale[1] = scale[1] / ratio_to_comp * image_width / image_height
+
+        self.get_prop_keyframe(context, 'position', ae_transform[0:3], time)
+        self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time)
+        self.get_prop_keyframe(context, 'scale', scale, time)
+        self.get_prop_keyframe(context, 'opacity', opacity, time)
+
+    def get_type_script(self):
+        type_script = f'var newFootage = app.project.importFile(new ImportOptions(File("{self.filepath}")));\n'
+        type_script += 'newFootage.parentFolder = footageFolder;\n'
+        type_script += f'var {self.name_ae} = newComp.layers.add(newFootage);\n'
+        type_script += f'{self.name_ae}.threeDLayer = true;\n'
+        type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n'
+        return type_script
+
+
+class SolidExport(ObjectExport):
+    def get_keyframe(self, context, data, time, ae_size):
+        # Convert obj transform properties to AE space
+        plane_matrix = get_plane_matrix(self.obj)
+        # Scale plane to account for AE's transforms
+        plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4)
+
+        ae_transform = convert_transform_matrix(plane_matrix, data['width'],
+                                                data['height'], data['aspect'],
+                                                True, ae_size)
+        opacity = 0.0 if self.obj.hide_render else 100.0
+        if not hasattr(self, 'color'):
+            self.color = get_plane_color(self.obj)
+        if not hasattr(self, 'width'):
+            self.width = data['width']
+        if not hasattr(self, 'height'):
+            self.height = data['height']
+
+        scale = ae_transform[6:9]
+        scale[1] *= data['width'] / data['height']
+
+        self.get_prop_keyframe(context, 'position', ae_transform[0:3], time)
+        self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time)
+        self.get_prop_keyframe(context, 'scale', scale, time)
+        self.get_prop_keyframe(context, 'opacity', opacity, time)
+
+    def get_type_script(self):
+        type_script = f'var {self.name_ae} = newComp.layers.addSolid({self.color},"{self.name_ae}",{self.width},{self.height},1.0);\n'
+        type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n'
+        type_script += f'{self.name_ae}.source.parentFolder = footageFolder;\n'
+        type_script += f'{self.name_ae}.threeDLayer = true;\n'
+        return type_script
+
+
+class CamBundleExport(ObjectExport):
+    def __init__(self, obj, track):
+        self.obj = obj
+        self.track = track
+        self.name_ae = convert_name(f'{obj.name}__{track.name}')
+        self.keyframes = {}
+
+    def get_keyframe(self, context, data, time, ae_size):
+        # Bundles are in camera space.
+        # Transpose to world space
+        matrix = Matrix.Translation(self.obj.matrix_basis
+                                    @ self.track.bundle)
+        # Convert the position into AE space
+        ae_transform = convert_transform_matrix(matrix, data['width'],
+                                                data['height'],
+                                                data['aspect'], False,
+                                                ae_size)
+
+        self.get_prop_keyframe(context, 'position', ae_transform[0:3], time)
+
+    def get_type_script(self):
+        type_script = f'var {self.name_ae} = newComp.layers.addNull();\n'
+        type_script += f'{self.name_ae}.threeDLayer = true;\n'
+        type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n'
+        return type_script
+
+
+def get_camera_bundles(scene, camera):
+    cam_bundles = []
+
+    for constraint in camera.constraints:
+        if constraint.type == 'CAMERA_SOLVER':
+            # Which movie clip does it use
+            if constraint.use_active_clip:
+                clip = scene.active_clip
+            else:
+                clip = constraint.clip
 
-        elif is_plane(ob):
-            solids.append(ob)
+            # Go through each tracking point
+            for track in clip.tracking.tracks:
+                # Does this tracking point have a bundle
+                # (has its 3D position been solved)
+                if track.has_bundle:
+                    cam_bundles.append(CamBundleExport(camera, track))
 
-        elif ob.type == 'LIGHT':
-            lights.append(ob)
+    return cam_bundles
 
-        else:
-            nulls.append(ob)
-
-    selection = {
-        'cameras': cameras,
-        'images': images,
-        'solids': solids,
-        'lights': lights,
-        'nulls': nulls,
-        }
 
-    return selection
+def get_selected(context, include_active_cam, include_selected_cams,
+                 include_selected_objects, include_cam_bundles,
+                 include_image_planes, include_solids):
+    """Create manageable list of selected objects"""
+    cameras = []
+    solids = []       # Meshes exported as AE solids
+    images = []       # Meshes exported as AE AV layers
+    lights = []       # Lights exported as AE lights
+    cam_bundles = []  # Camera trackers exported as AE nulls
+    nulls = []        # Remaining objects exported as AE nulls
+
+    if context.scene.camera is not None:
+        if include_active_cam:
+            cameras.append(CameraExport(context.scene.camera))
+        if include_cam_bundles:
+            cam_bundles.extend(get_camera_bundles(context.scene, context.scene.camera))
+
+    for obj in context.selected_objects:
+        if obj.type == 'CAMERA':
+            if (include_active_cam
+                    and obj is context.scene.camera):
+                # Ignore active camera if already selected
+                continue
+            else:
+                if include_selected_cams:
+                    cameras.append(CameraExport(obj))
+                if include_cam_bundles:
+                    cam_bundles.extend(get_camera_bundles(context.scene, obj))
+
+        elif include_image_planes and is_image_plane(obj):
+            images.append(ImageExport(obj))
+
+        elif include_solids and is_plane(obj):
+            solids.append(SolidExport(obj))
+
+        elif include_selected_objects:
+            if obj.type == 'LIGHT':
+                lights.append(LightExport(obj))
+            else:
+                nulls.append(ObjectExport(obj))
+
+    return {'cameras': cameras,
+            'images': images,
+            'solids': solids,
+            'lights': lights,
+            'nulls': nulls,
+            'cam_bundles': cam_bundles}
 
 
 def get_first_material(obj):
@@ -276,14 +521,8 @@ def get_image_plane_matrix(obj):
 
 def convert_name(name):
     """Convert names of objects to avoid errors in AE"""
-    name = "_" + name
-    '''
-    # Digits are not allowed at beginning of AE vars names.
-    # This section is commented, as "_" is added at beginning of names anyway.
-    # Placeholder for this name modification is left so that it's not ignored if needed
-    if name[0].isdigit():
+    if not name[0].isalpha():
         name = "_" + name
-    '''
     name = bpy.path.clean_name(name)
     name = name.replace("-", "_")
 
@@ -300,7 +539,7 @@ def convert_transform_matrix(matrix, width, height, aspect,
 
     scale_mat = Matrix.Scale(width, 4)
 
-    # Get blender transform data for ob
+    # Get blender transform data for object
     b_loc = matrix.to_translation()
     b_rot = matrix.to_euler('ZYX')  # ZYX euler matches AE's orientation and allows to use x_rot_correction
     b_scale = matrix.to_scale()
@@ -319,7 +558,7 @@ def convert_transform_matrix(matrix, width, height, aspect,
     ry = -degrees(b_rot.y)  # AE's Y orientation = -blender's Y rotation if 'ZYX' euler
     rz = -degrees(b_rot.z)  # AE's Z orientation = -blender's Z rotation if 'ZYX' euler
     if x_rot_correction:
-        # In Blender, ob of zero rotation lays on floor.
+        # In Blender, object of zero rotation lays on floor.
         # In AE, layer of zero orientation "stands"
         rx -= 90.0
     # Convert scale to AE scale. ae_size is a global multiplier.
@@ -327,7 +566,8 @@ def convert_transform_matrix(matrix, width, height, aspect,
     sy = b_scale.y * ae_size
     sz = b_scale.z * ae_size
 
-    return x, y, z, rx, ry, rz, sx, sy, sz
+    return [x, y, z, rx, ry, rz, sx, sy, sz]
+
 
 # Get camera's lens and convert to AE's "zoom" value in pixels
 # this function will be called for every camera for every frame
@@ -387,7 +627,6 @@ def convert_transform_matrix(matrix, width, height, aspect,
 # aspect compensation is needed, so final formula is:
 #     zoom = lens * dimension / sensor * aspect
 
-
 def convert_lens(camera, width, height, aspect):
     if camera.data.sensor_fit == 'VERTICAL':
         sensor = camera.data.sensor_height
@@ -411,612 +650,86 @@ def convert_lens(camera, width, height, aspect):
 #    return matrix
 
 
-def write_jsx_file(file, data, selection, include_animation,
-                   include_active_cam, include_selected_cams,
-                   include_selected_objects, include_cam_bundles,
-                   include_image_planes, ae_size):
+def write_jsx_file(context, file, data, selection, include_animation, ae_size):
     """jsx script for AE creation"""
 
-    print("\n---------------------------\n- Export to After Effects -\n---------------------------")
-    # Store the current frame to restore it at the end of export
-    curframe = data['curframe']
-    # Create array which will contain all keyframes values
-    js_data = {
-        'times': '',
-        'cameras': {},
-        'images': {},
-        'solids': {},
-        'lights': {},
-        'nulls': {},
-        'bundles_cam': {},
-        'bundles_ob': {},  # not ready yet
-        }
-
-    # Create structure for active camera/cameras
-    active_cam_name = ''
-    if include_active_cam and data['active_cam_frames']:
-        # Check if more than one active cam exists
-        # (True if active cams set by markers)
-        if len(data['active_cam_frames']) == 1:
-            # Take name of the only active camera in scene
-            name_ae = convert_name(data['active_cam_frames'][0].name)
-        else:
-            name_ae = 'Active_Camera'
-        # Store name to be used when creating keyframes for active cam
-        active_cam_name = name_ae
-        js_data['cameras'][name_ae] = {
-            'position': '',
-            'position_static': '',
-            'position_anim': False,
-            'orientation': '',
-            'orientation_static': '',
-            'orientation_anim': False,
-            'zoom': '',
-            'zoom_static': '',
-            'zoom_anim': False,
-            }
-
-    # Create camera structure for selected cameras
-    if include_selected_cams:
-        for obj in selection['cameras']:
-            # More than one camera can be selected
-            if convert_name(obj.name) != active_cam_name:
-                name_ae = convert_name(obj.name)
-                js_data['cameras'][name_ae] = {
-                    'position': '',
-                    'position_static': '',
-                    'position_anim': False,
-                    'orientation': '',
-                    'orientation_static': '',
-                    'orientation_anim': False,
-                    'zoom': '',
-                    'zoom_static': '',
-                    'zoom_anim': False,
-                    }
-
-    # Create structure for solids
-    for obj in selection['solids']:
-        name_ae = convert_name(obj.name)
-        js_data['solids'][name_ae] = {
-            'position': '',
-            'position_static': '',
-            'position_anim': False,
-            'orientation': '',
-            'orientation_static': '',
-            'orientation_anim': False,
-            'scale': '',
-            'scale_static': '',
-            'scale_anim': False,
-            'opacity': '',
-            'opacity_static': '',
-            'opacity_anim': False,
-            }
-
-    # Create structure for images
-    for obj in selection['images']:
-        name_ae = convert_name(obj.name)
-        js_data['images'][name_ae] = {
-            'position': '',
-            'position_static': '',
-            'position_anim': False,
-            'orientation': '',
-            'orientation_static': '',
-            'orientation_anim': False,
-            'scale': '',
-            'scale_static': '',
-            'scale_anim': False,
-            'opacity': '',
-            'opacity_static': '',
-            'opacity_anim': False,
-            'filepath': '',
-        }
+    print("\n---------------------------\n"
+          "- Export to After Effects -\n"
+          "---------------------------")
 
-    # Create structure for lights
-    for obj in selection['lights']:
-        if include_selected_objects:
-            name_ae = obj.data.type + convert_name(obj.name)
-            js_data['lights'][name_ae] = {
-                'type': obj.data.type,
-                'intensity': '',
-                'intensity_static': '',
-                'intensity_anim': False,
-                'Cone Angle': '',
-                'Cone Angle_static': '',
-                'Cone Angle_anim': False,
-                'Cone Feather': '',
-                'Cone Feather_static': '',
-                'Cone Feather_anim': False,
-                'Color': '',
-                'Color_static': '',
-                'Color_anim': False,
-                'position': '',
-                'position_static': '',
-                'position_anim': False,
-                'orientation': '',
-                'orientation_static': '',
-                'orientation_anim': False,
-                'opacity': '',
-                'opacity_static': '',
-                'opacity_anim': False,
-                }
-
-    # Create structure for nulls
-    # nulls representing blender's obs except cameras, lights and solids
-    for obj in selection['nulls']:
-        if include_selected_objects:
-            name_ae = convert_name(obj.name)
-            js_data['nulls'][name_ae] = {
-                'position': '',
-                'position_static': '',
-                'position_anim': False,
-                'orientation': '',
-                'orientation_static': '',
-                'orientation_anim': False,
-                'scale': '',
-                'scale_static': '',
-                'scale_anim': False,
-                }
-
-    # Create structure for cam bundles including positions
-    # (cam bundles don't move)
-    if include_cam_bundles:
-        # Go through each selected camera and active cameras
-        selected_cams = []
-        active_cams = []
-        if include_active_cam:
-            active_cams = data['active_cam_frames']
-        if include_selected_cams:
-            for cam in selection['cameras']:
-                selected_cams.append(cam)
-        # List of cameras that will be checked for 'CAMERA SOLVER'
-        cams = list(set.union(set(selected_cams), set(active_cams)))
-
-        for cam in cams:
-            # Go through each constraints of this camera
-            for constraint in cam.constraints:
-                # Does the camera have a Camera Solver constraint
-                if constraint.type == 'CAMERA_SOLVER':
-                    # Which movie clip does it use
-                    if constraint.use_active_clip:
-                        clip = data['scn'].active_clip
-                    else:
-                        clip = constraint.clip
-
-                    # Go through each tracking point
-                    for track in clip.tracking.tracks:
-                        # Does this tracking point have a bundle
-                        # (has its 3D position been solved)
-                        if track.has_bundle:
-                            # Get the name of the tracker
-                            name_ae = convert_name(str(cam.name) + '__' +
-                                                   str(track.name))
-                            js_data['bundles_cam'][name_ae] = {
-                                'position': '',
-                                }
-                            # Bundles are in camera space.
-                            # Transpose to world space
-                            matrix = Matrix.Translation(cam.matrix_basis.copy()
-                                                        @ track.bundle)
-                            # Convert the position into AE space
-                            ae_transform = (convert_transform_matrix(
-                                matrix, data['width'], data['height'],
-                                data['aspect'], False, ae_size))
-                            js_data['bundles_cam'][name_ae]['position'] += f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],'
+    # Store the current frame to restore it at the end of export
+    frame_current = data['frame_current']
 
     # Get all keyframes for each object and store in dico
     if include_animation:
         end = data['end'] + 1
     else:
         end = data['start'] + 1
+
     for frame in range(data['start'], end):
-        print("working on frame: " + str(frame))
+        print("Working on frame: " + str(frame))
         data['scn'].frame_set(frame)
 
         # Get time for this loop
-        js_data['times'] += str((frame - data['start']) / data['fps']) + ','
+        time = (frame - data['start']) / data['fps']
 
-        # Keyframes for active camera/cameras
-        if include_active_cam and data['active_cam_frames'] != []:
-            if len(data['active_cam_frames']) == 1:
-                cur_cam_index = 0
-            else:
-                cur_cam_index = frame - data['start']
-            active_cam = data['active_cam_frames'][cur_cam_index]
-            # Get cam name
-            name_ae = active_cam_name
-            # Convert cam transform properties to AE space
-            ae_transform = (convert_transform_matrix(
-                active_cam.matrix_world.copy(), data['width'], data['height'],
-                data['aspect'], True, ae_size))
-            # Convert Blender's lens to AE's zoom in pixels
-            zoom = convert_lens(active_cam, data['width'], data['height'],
-                                data['aspect'])
-            # Store all values in dico
-
-            position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],'
-            orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],'
-            zoom = str(zoom) + ','
-            js_camera = js_data['cameras'][name_ae]
-            js_camera['position'] += position
-            js_camera['orientation'] += orientation
-            js_camera['zoom'] += zoom
-            # Check if properties change values compared to previous frame
-            # If property don't change through out the whole animation,
-            # keyframes won't be added
-            if frame != data['start']:
-                if position != js_camera['position_static']:
-                    js_camera['position_anim'] = True
-                if orientation != js_camera['orientation_static']:
-                    js_camera['orientation_anim'] = True
-                if zoom != js_camera['zoom_static']:
-                    js_camera['zoom_anim'] = True
-            js_camera['position_static'] = position
-            js_camera['orientation_static'] = orientation
-            js_camera['zoom_static'] = zoom
-
-        # Keyframes for selected cameras
-        if include_selected_cams:
-            for obj in selection['cameras']:
-                if convert_name(obj.name) != active_cam_name:
-                    # Get cam name
-                    name_ae = convert_name(obj.name)
-                    # Convert cam transform properties to AE space
-                    ae_transform = convert_transform_matrix(
-                        obj.matrix_world.copy(), data['width'],
-                        data['height'], data['aspect'], True, ae_size)
-                    # Convert Blender's lens to AE's zoom in pixels
-                    zoom = convert_lens(obj, data['width'], data['height'],
-                                        data['aspect'])
-                    # Store all values in dico
-                    position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],'
-                    orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],'
-                    zoom = str(zoom) + ','
-                    js_camera = js_data['cameras'][name_ae]
-                    js_camera['position'] += position
-                    js_camera['orientation'] += orientation
-                    js_camera['zoom'] += zoom
-                    # Check if properties change values compared to previous frame
-                    # If property don't change through out the whole animation,
-                    # keyframes won't be added
-                    if frame != data['start']:
-                        if position != js_camera['position_static']:
-                            js_camera['position_anim'] = True
-                        if orientation != js_camera['orientation_static']:
-                            js_camera['orientation_anim'] = True
-                        if zoom != js_camera['zoom_static']:
-                            js_camera['zoom_anim'] = True
-                    js_camera['position_static'] = position
-                    js_camera['orientation_static'] = orientation
-                    js_camera['zoom_static'] = zoom
-
-        # Keyframes for all solids.
-        if include_selected_objects:
-            for obj in selection['solids']:
-                # Get object name
-                name_ae = convert_name(obj.name)
-                # Convert obj transform properties to AE space
-                plane_matrix = get_plane_matrix(obj)
-                # Scale plane to account for AE's transforms
-                plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4)
-                ae_transform = convert_transform_matrix(
-                    plane_matrix, data['width'], data['height'],
-                    data['aspect'], True, ae_size)
-                # Store all values in dico
-                position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],'
-                orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],'
-                scale = f'[{ae_transform[6]},{ae_transform[7] * data["width"] / data["height"]},{ae_transform[8]}],'
-                opacity = '0.0,' if obj.hide_render else '100.0,'
-                js_solid = js_data['solids'][name_ae]
-                js_solid['color'] = get_plane_color(obj)
-                js_solid['width'] = data['width']
-                js_solid['height'] = data['height']
-                js_solid['position'] += position
-                js_solid['orientation'] += orientation
-                js_solid['scale'] += scale
-                js_solid['opacity'] += opacity
-                # Check if properties change values compared to previous frame
-                # If property don't change through out the whole animation,
-                # keyframes won't be added
-                if frame != data['start']:
-                    if position != js_solid['position_static']:
-                        js_solid['position_anim'] = True
-                    if orientation != js_solid['orientation_static']:
-                        js_solid['orientation_anim'] = True
-                    if scale != js_solid['scale_static']:
-                        js_solid['scale_anim'] = True
-                    if opacity != js_solid['opacity_static']:
-                        js_solid['opacity_anim'] = True
-                js_solid['position_static'] = position
-                js_solid['orientation_static'] = orientation
-                js_solid['scale_static'] = scale
-                js_solid['opacity_static'] = opacity
-
-        # Keyframes for all lights.
-        if include_selected_objects:
-            for obj in selection['lights']:
-                # Get object name
-                name_ae = obj.data.type + convert_name(obj.name)
-                type = obj.data.type
-                # Convert ob transform properties to AE space
-                ae_transform = convert_transform_matrix(
-                    obj.matrix_world.copy(), data['width'], data['height'],
-                    data['aspect'], True, ae_size)
-                color = obj.data.color
-                # Store all values in dico
-                position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],'
-                orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],'
-                energy = f'[{obj.data.energy * 100.0}],'
-                color = f'[{color[0]},{color[1]},{color[2]}],'
-                opacity = '0.0,' if obj.hide_render else '100.0,'
-                js_light = js_data['lights'][name_ae]
-                js_light['position'] += position
-                js_light['orientation'] += orientation
-                js_light['intensity'] += energy
-                js_light['Color'] += color
-                js_light['opacity'] += opacity
-                # Check if properties change values compared to previous frame
-                # If property don't change through out the whole animation,
-                # keyframes won't be added
-                if frame != data['start']:
-                    if position != js_light['position_static']:
-                        js_light['position_anim'] = True
-                    if orientation != js_light['orientation_static']:
-                        js_light['orientation_anim'] = True
-                    if energy != js_light['intensity_static']:
-                        js_light['intensity_anim'] = True
-                    if color != js_light['Color_static']:
-                        js_light['Color_anim'] = True
-                    if opacity != js_light['opacity_static']:
-                        js_light['opacity_anim'] = True
-                js_light['position_static'] = position
-                js_light['orientation_static'] = orientation
-                js_light['intensity_static'] = energy
-                js_light['Color_static'] = color
-                js_light['opacity_static'] = opacity
-                if type == 'SPOT':
-                    cone_angle = f'[{degrees(obj.data.spot_size)}],'
-                    cone_feather = f'[obj.data.spot_blend * 100.0],'
-                    js_light['Cone Angle'] += cone_angle
-                    js_light['Cone Feather'] += cone_feather
-                    # Check if properties change values compared to previous frame
-                    # If property don't change through out the whole animation,
-                    # keyframes won't be added
-                    if frame != data['start']:
-                        if cone_angle != js_light['Cone Angle_static']:
-                            js_light['Cone Angle_anim'] = True
-                        if cone_feather != js_light['Cone Feather_static']:
-                            js_light['Cone Feather_anim'] = True
-                    js_light['Cone Angle_static'] = cone_angle
-                    js_light['Cone Feather_static'] = cone_feather
-
-        # Keyframes for all nulls
-        if include_selected_objects:
-            for obj in selection['nulls']:
-                # Get object name
-                name_ae = convert_name(obj.name)
-                # Convert obj transform properties to AE space
-                ae_transform = convert_transform_matrix(obj.matrix_world.copy(), data['width'], data['height'], data['aspect'], True, ae_size)
-                # Store all values in dico
-                position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],'
-                orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],'
-                scale = f'[{ae_transform[6]},{ae_transform[7]},{ae_transform[8]}],'
-                js_null = js_data['nulls'][name_ae]
-                js_null['position'] += position
-                js_null['orientation'] += orientation
-                js_null['scale'] += scale
-                # Check if properties change values compared to previous frame
-                # If property don't change through out the whole animation,
-                # keyframes won't be added
-                if frame != data['start']:
-                    if position != js_null['position_static']:
-                        js_null['position_anim'] = True
-                    if orientation != js_null['orientation_static']:
-                        js_null['orientation_anim'] = True
-                    if scale != js_null['scale_static']:
-                        js_null['scale_anim'] = True
-                js_null['position_static'] = position
-                js_null['orientation_static'] = orientation
-                js_null['scale_static'] = scale
-
-        # Keyframes for all images
-        if include_image_planes:
-            for obj in selection['images']:
-                # Get object name
-                name_ae = convert_name(obj.name)
-                # Convert obj transform properties to AE space
-                plane_matrix = get_image_plane_matrix(obj)
-                # Scale plane to account for AE's transforms
-                plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4)
-                ae_transform = convert_transform_matrix(
-                    plane_matrix, data['width'], data['height'],
-                    data['aspect'], True, ae_size)
-                # Store all values in dico
-                position = f'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],'
-                orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],'
-                image_width, image_height = get_image_size(obj)
-                ratio_to_comp = image_width / data['width']
-                scale = f'[{ae_transform[6] / ratio_to_comp},{ae_transform[7] / ratio_to_comp * image_width / image_height},{ae_transform[8]}],'
-                opacity = '0.0,' if obj.hide_render else '100.0,'
-                js_image = js_data['images'][name_ae]
-                js_image['position'] += position
-                js_image['orientation'] += orientation
-                js_image['scale'] += scale
-                js_image['opacity'] += opacity
-                # Check if properties change values compared to previous frame
-                # If property don't change through out the whole animation,
-                # keyframes won't be added
-                if frame != data['start']:
-                    if position != js_image['position_static']:
-                        js_image['position_anim'] = True
-                    if orientation != js_image['orientation_static']:
-                        js_image['orientation_anim'] = True
-                    if scale != js_image['scale_static']:
-                        js_image['scale_anim'] = True
-                    if opacity != js_image['opacity_static']:
-                        js_image['opacity_anim'] = True
-                js_image['position_static'] = position
-                js_image['orientation_static'] = orientation
-                js_image['scale_static'] = scale
-                js_image['opacity_static'] = opacity
-                js_image['filepath'] = get_image_filepath(obj)
-
-        # keyframes for all object bundles. Not ready yet.
-        #
-        #
-        #
+        for obj_type in selection.values():
+            for obj in obj_type:
+                obj.get_keyframe(context, data, time, ae_size)
 
     # ---- write JSX file
-    jsx_file = open(file, 'w')
-
-    # Make the jsx executable in After Effects (enable double click on jsx)
-    jsx_file.write('#target AfterEffects\n\n')
-    # Script's header
-    jsx_file.write('/**************************************\n')
-    jsx_file.write(f'Scene : {data["scn"].name}\n')
-    jsx_file.write(f'Resolution : {data["width"]} x {data["height"]}\n')
-    jsx_file.write(f'Duration : {data["duration"]}\n')
-    jsx_file.write(f'FPS : {data["fps"]}\n')
-    jsx_file.write(f'Date : {datetime.datetime.now()}\n')
-    jsx_file.write(f'Exported with io_export_after_effects.py\n')
-    jsx_file.write(f'**************************************/\n\n\n\n')
-
-    # Wrap in function
-    jsx_file.write("function compFromBlender(){\n")
-    # Create new comp
-    if bpy.data.filepath:
-        comp_name = convert_name(
-            os.path.splitext(os.path.basename(bpy.data.filepath))[0])
-    else:
-        comp_name = "BlendComp"
-    jsx_file.write(f'\nvar compName = prompt("Blender Comp\'s Name \\nEnter Name of newly created Composition","{comp_name}","Composition\'s Name");\n')
-    jsx_file.write('if (compName){')
-    # Continue only if comp name is given. If not - terminate
-    jsx_file.write(
-        f'\nvar newComp = app.project.items.addComp(compName, {data["width"]}, '
-        f'{data["height"]}, {data["aspect"]}, {data["duration"]}, {data["fps"]});')
-    jsx_file.write(f"\nnewComp.displayStartTime = {(data['start']) / data['fps']};\n\n")
-
-    jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n')
-
-    # Create camera bundles (nulls)
-    jsx_file.write('// **************  CAMERA 3D MARKERS  **************\n\n')
-    for name_ae, obj in js_data['bundles_cam'].items():
-        jsx_file.write(f'var {name_ae} = newComp.layers.addNull();\n')
-        jsx_file.write(f'{name_ae}.threeDLayer = true;\n')
-        jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n')
-        jsx_file.write(f'{name_ae}.property("position").setValue({obj["position"]});\n\n')
-    jsx_file.write('\n')
-
-    # Create object bundles (not ready yet)
-
-    # Create objects (nulls)
-    jsx_file.write('// **************  OBJECTS  **************\n\n')
-    for name_ae, obj in js_data['nulls'].items():
-        jsx_file.write(f'var {name_ae} = newComp.layers.addNull();\n')
-        jsx_file.write(f'{name_ae}.threeDLayer = true;\n')
-        jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n')
-        # Set values of properties, add keyframes only where needed
-        for prop in ("position", "orientation", "scale"):
-            if include_animation and obj[prop + '_anim']:
-                jsx_file.write(
-                    f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n')
-            else:
-                jsx_file.write(
-                    f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n')
-        jsx_file.write('\n')
-    jsx_file.write('\n')
-
-    # Create solids
-    jsx_file.write('// **************  SOLIDS  **************\n\n')
-    for name_ae, obj in js_data['solids'].items():
-        jsx_file.write(
-            f'var {name_ae} = newComp.layers.addSolid({obj["color"]},"{name_ae}",{obj["width"]},{obj["height"]},1.0);\n')
-        jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n')
-        jsx_file.write(f'{name_ae}.source.parentFolder = footageFolder;\n')
-        jsx_file.write(f'{name_ae}.threeDLayer = true;\n')
-        # Set values of properties, add keyframes only where needed
-        for prop in ("position", "orientation", "scale", "opacity"):
-            if include_animation and obj[prop + '_anim']:
-                jsx_file.write(
-                    f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n')
-            else:
-                jsx_file.write(
-                    f'{name_ae}.property("{prop}").setValue([{obj[prop + "_static"]});\n')
-        jsx_file.write('\n')
-    jsx_file.write('\n')
-
-    # Create images
-    jsx_file.write('// **************  IMAGES  **************\n\n')
-    for name_ae, obj in js_data['images'].items():
-        jsx_file.write(
-            f'var newFootage = app.project.importFile(new ImportOptions(File("{obj["filepath"]}")));\n')
-        jsx_file.write('newFootage.parentFolder = footageFolder;\n')
-        jsx_file.write(
-            f'var {name_ae} = newComp.layers.add(newFootage);\n')
-        jsx_file.write(f'{name_ae}.threeDLayer = true;\n')
-        jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n')
-        # Set values of properties, add keyframes only where needed
-        for prop in ("position", "orientation", "scale", "opacity"):
-            if include_animation and obj[prop + '_anim']:
-                jsx_file.write(
-                    f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n')
-            else:
-                jsx_file.write(f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n')
-        jsx_file.write('\n')
-    jsx_file.write('\n')
-
-    # Create lights
-    jsx_file.write('// **************  LIGHTS  **************\n\n')
-    for name_ae, obj in js_data['lights'].items():
-        jsx_file.write(
-            f'var {name_ae} = newComp.layers.addLight("{name_ae}", [0.0, 0.0]);\n')
-        jsx_file.write(
-            f'{name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n')
-        # Set values of properties, add keyframes only where needed
-        props = ["position", "orientation", "intensity", "Color", "opacity"]
-        if obj['type'] == 'SPOT':
-            props.extend(("Cone Angle", "Cone Feather"))
-        for prop in props:
-            if include_animation and obj[prop + '_anim']:
-                jsx_file.write(
-                    f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n')
-            else:
-                jsx_file.write(
-                    f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n')
-        jsx_file.write('\n')
-    jsx_file.write('\n')
-
-    # Create cameras
-    jsx_file.write('// **************  CAMERAS  **************\n\n')
-    for name_ae, obj in js_data['cameras'].items():
-        # More than one camera can be selected
-        jsx_file.write(
-            f'var {name_ae} = newComp.layers.addCamera("{name_ae}",[0,0]);\n')
+    with open(file, 'w') as jsx_file:
+
+        # Make the jsx executable in After Effects (enable double click on jsx)
+        jsx_file.write('#target AfterEffects\n\n')
+        # Script's header
+        jsx_file.write('/**************************************\n')
+        jsx_file.write(f'Scene : {data["scn"].name}\n')
+        jsx_file.write(f'Resolution : {data["width"]} x {data["height"]}\n')
+        jsx_file.write(f'Duration : {data["duration"]}\n')
+        jsx_file.write(f'FPS : {data["fps"]}\n')
+        jsx_file.write(f'Date : {datetime.datetime.now()}\n')
+        jsx_file.write(f'Exported with io_export_after_effects.py\n')
+        jsx_file.write(f'**************************************/\n\n\n\n')
+
+        # Wrap in function
+        jsx_file.write("function compFromBlender(){\n")
+
+        # Create new comp
+        if bpy.data.filepath:
+            comp_name = convert_name(
+                os.path.splitext(os.path.basename(bpy.data.filepath))[0])
+        else:
+            comp_name = "BlendComp"
+        jsx_file.write(f'\nvar compName = prompt("Blender Comp\'s Name \\nEnter Name of newly created Composition","{comp_name}","Composition\'s Name");\n')
+        jsx_file.write('if (compName){')
+        # Continue only if comp name is given. If not - terminate
         jsx_file.write(
-            f'{name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n')
-
-        # Set values of properties, add keyframes only where needed
-        for prop in ("position", "orientation", "zoom"):
-            if include_animation and obj[prop + '_anim']:
-                jsx_file.write(
-                    f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n')
-            else:
-                jsx_file.write(f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n')
-        jsx_file.write('\n')
-    jsx_file.write('\n')
-
-    # Exit import if no comp name given
-    jsx_file.write('\n}else{alert ("Exit Import Blender animation data \\nNo Comp name has been chosen","EXIT")};')
-    # Close function
-    jsx_file.write("}\n\n\n")
-    # Execute function. Wrap in "undo group" for easy undoing import process
-    jsx_file.write('app.beginUndoGroup("Import Blender animation data");\n')
-    jsx_file.write('compFromBlender();\n')  # Execute function
-    jsx_file.write('app.endUndoGroup();\n\n\n')
-    jsx_file.close()
+            f'\nvar newComp = app.project.items.addComp(compName, {data["width"]}, '
+            f'{data["height"]}, {data["aspect"]}, {data["duration"]}, {data["fps"]});')
+        jsx_file.write(f"\nnewComp.displayStartTime = {(data['start']) / data['fps']};\n\n")
+
+        jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n')
+
+        for obj_type in ('cam_bundles', 'nulls', 'solids', 'images', 'lights', 'cameras'):
+            if len(selection[obj_type]):
+                type_name = 'CAMERA 3D MARKERS' if obj_type == 'cam_bundles' else obj_type.upper()
+                jsx_file.write(f'// **************  {type_name}  **************\n\n')
+                for obj in selection[obj_type]:
+                    jsx_file.write(obj.get_obj_script(include_animation))
+                jsx_file.write('\n')
+
+        # Exit import if no comp name given
+        jsx_file.write('\n}else{alert ("Exit Import Blender animation data \\nNo Comp name has been chosen","EXIT")};')
+        # Close function
+        jsx_file.write("}\n\n\n")
+        # Execute function. Wrap in "undo group" for easy undoing import process
+        jsx_file.write('app.beginUndoGroup("Import Blender animation data");\n')
+        jsx_file.write('compFromBlender();\n')  # Execute function
+        jsx_file.write('app.endUndoGroup();\n\n\n')
 
     # Set current frame of animation in blender to state before export
-    data['scn'].frame_set(curframe)
+    data['scn'].frame_set(frame_current)
 
 
 ##########################################
@@ -1066,6 +779,11 @@ class ExportJsx(bpy.types.Operator, ExportHelper):
             description="Include image mesh objects",
             default=True,
             )
+    include_solids: BoolProperty(
+            name="Solids",
+            description="Include rectangles as solids",
+            default=True,
+            )
 #    include_ob_bundles = BoolProperty(
 #            name="Objects 3D Markers",
 #            description="Include 3D Markers of Object Motion Solution for selected cameras",
@@ -1084,19 +802,24 @@ class ExportJsx(bpy.types.Operator, ExportHelper):
 
         box = layout.box()
         box.label(text='Include Cameras and Objects')
-        box.prop(self, 'include_active_cam')
-        box.prop(self, 'include_selected_cams')
-        box.prop(self, 'include_selected_objects')
-        box.prop(self, 'include_image_planes')
+        col = box.column(align=True)
+        col.prop(self, 'include_active_cam')
+        col.prop(self, 'include_selected_cams')
+        col.prop(self, 'include_selected_objects')
+        col.prop(self, 'include_image_planes')
+        col.prop(self, 'include_solids')
+
+        box = layout.box()
+        box.label(text='Include Tracking Data')
+        box.prop(self, 'include_cam_bundles')
+#        box.prop(self, 'include_ob_bundles')
+
         box = layout.box()
         box.prop(self, 'include_animation')
+
         box = layout.box()
         box.label(text='Transform')
         box.prop(self, 'ae_size')
-        box = layout.box()
-        box.label(text='Include Tracking Data:')
-        box.prop(self, 'include_cam_bundles')
-#        box.prop(self, 'include_ob_bundles')
 
     @classmethod
     def poll(cls, context):
@@ -1106,11 +829,14 @@ class ExportJsx(bpy.types.Operator, ExportHelper):
 
     def execute(self, context):
         data = get_comp_data(context)
-        selection = get_selected(context)
-        write_jsx_file(self.filepath, data, selection, self.include_animation,
-                       self.include_active_cam, self.include_selected_cams,
-                       self.include_selected_objects, self.include_cam_bundles,
-                       self.include_image_planes, self.ae_size)
+        selection = get_selected(context, self.include_active_cam,
+                                 self.include_selected_cams,
+                                 self.include_selected_objects,
+                                 self.include_cam_bundles,
+                                 self.include_image_planes,
+                                 self.include_solids)
+        write_jsx_file(context, self.filepath, data, selection,
+                       self.include_animation, self.ae_size)
         print("\nExport to After Effects Completed")
         return {'FINISHED'}