From afb79c2611c0ffe6a09cd35d899c6e2bad73d59b Mon Sep 17 00:00:00 2001
From: Damien Picard <dam.pic@free.fr>
Date: Tue, 15 Jun 2021 10:27:31 +0200
Subject: [PATCH] After Effects export: refactor object types into classes

Use classes for each object type, which include data and animation
collection, and script generation, instead of long and similar
functions for each of these steps.

Also:
- add option to export solids;
- do not systematically prefix all AE object names with an underscore,
  as this is not so useful and can be frustrating in comps.
---
 io_export_after_effects.py | 1010 +++++++++++++-----------------------
 1 file changed, 368 insertions(+), 642 deletions(-)

diff --git a/io_export_after_effects.py b/io_export_after_effects.py
index 8d73d193..4861de9e 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'}
 
-- 
GitLab