├── .pylintrc ├── .gitignore ├── docs ├── ae-step1.png ├── ae-step2.png ├── ae-step3.png ├── ae-step4.png ├── ae-step5.png ├── blender-step1.png ├── blender-step2.png ├── blender-step3.png ├── blender-step4.png └── ae-centered-comp-camera.png ├── .vscode └── settings.json ├── .github ├── ISSUE_TEMPLATE │ ├── something-else.md │ ├── feature_request.md │ ├── issue-exporting-from-after-effects.md │ ├── issue-importing-into-blender.md │ └── issue-in-import-export.md └── workflows │ └── create-release.yml ├── export-comp-from-ae ├── README.md ├── lib │ ├── util.js │ └── json2.js └── export-comp-from-ae.jsx ├── import-comp-to-blender ├── README.md ├── blender_manifest.toml ├── legacy_error.py └── __init__.py ├── util ├── build-blender.py └── build-ae.py ├── README.md └── LICENSE_JS /.pylintrc: -------------------------------------------------------------------------------- 1 | additional-builtins=bpy -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/.ropeproject 2 | build -------------------------------------------------------------------------------- /docs/ae-step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adroitwhiz/after-effects-to-blender-export/HEAD/docs/ae-step1.png -------------------------------------------------------------------------------- /docs/ae-step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adroitwhiz/after-effects-to-blender-export/HEAD/docs/ae-step2.png -------------------------------------------------------------------------------- /docs/ae-step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adroitwhiz/after-effects-to-blender-export/HEAD/docs/ae-step3.png -------------------------------------------------------------------------------- /docs/ae-step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adroitwhiz/after-effects-to-blender-export/HEAD/docs/ae-step4.png -------------------------------------------------------------------------------- /docs/ae-step5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adroitwhiz/after-effects-to-blender-export/HEAD/docs/ae-step5.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.trimTrailingWhitespace": true, 3 | "python.linting.pylintEnabled": true 4 | } -------------------------------------------------------------------------------- /docs/blender-step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adroitwhiz/after-effects-to-blender-export/HEAD/docs/blender-step1.png -------------------------------------------------------------------------------- /docs/blender-step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adroitwhiz/after-effects-to-blender-export/HEAD/docs/blender-step2.png -------------------------------------------------------------------------------- /docs/blender-step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adroitwhiz/after-effects-to-blender-export/HEAD/docs/blender-step3.png -------------------------------------------------------------------------------- /docs/blender-step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adroitwhiz/after-effects-to-blender-export/HEAD/docs/blender-step4.png -------------------------------------------------------------------------------- /docs/ae-centered-comp-camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adroitwhiz/after-effects-to-blender-export/HEAD/docs/ae-centered-comp-camera.png -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/something-else.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Something else 3 | about: Something which doesn't fit into the above categories 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /export-comp-from-ae/README.md: -------------------------------------------------------------------------------- 1 | # STOP! 2 | 3 | **You cannot download the script from this folder.** 4 | 5 | If you want to download the After Effects script, you may do so on [**the Releases page**](https://github.com/adroitwhiz/after-effects-to-blender-export/releases). 6 | 7 | This folder only contains source code, and the script will not function if you try to install it from here. 8 | -------------------------------------------------------------------------------- /import-comp-to-blender/README.md: -------------------------------------------------------------------------------- 1 | # STOP! 2 | 3 | **You cannot download the extension from this folder.** 4 | 5 | If you want to download the Blender extension, you may do so on [**the Releases page**](https://github.com/adroitwhiz/after-effects-to-blender-export/releases). 6 | 7 | This folder only contains source code, and the extension will not function if you try to install it from here. 8 | -------------------------------------------------------------------------------- /import-comp-to-blender/blender_manifest.toml: -------------------------------------------------------------------------------- 1 | schema_version = "1.0.0" 2 | 3 | id = "after_effects_to_blender_import" 4 | version = "0.6.1" 5 | name = "Import After Effects Composition" 6 | tagline = "Import layers from an After Effects composition into Blender" 7 | maintainer = "adroitwhiz" 8 | type = "add-on" 9 | website = "https://github.com/adroitwhiz/after-effects-to-blender-export/" 10 | tags = ["Import-Export"] 11 | blender_version_min = "4.2.0" 12 | license = ["SPDX:GPL-3.0-or-later"] 13 | copyright = ["2020-2025 adroitwhiz"] 14 | 15 | [permissions] 16 | files = "Import .json files from disk" 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /util/build-blender.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Create a Blender addon .zip from the `import-comp-to-blender` folder. 3 | ''' 4 | 5 | import glob 6 | import zipfile 7 | import os 8 | 9 | def should_exclude_path(path: str): 10 | return path.endswith('.md') 11 | 12 | dst = zipfile.ZipFile('build/after_effects_to_blender_import.zip', "w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) 13 | 14 | for path in glob.iglob('**', root_dir='import-comp-to-blender', recursive=True): 15 | src_path = 'import-comp-to-blender/' + path 16 | # We don't need to include directories in the zip file, just their contents 17 | if os.path.isdir(src_path) or should_exclude_path(path): 18 | continue 19 | dst.write(src_path, path) 20 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v[0-9]+.[0-9]+.[0-9]+' 7 | - 'v[0-9]+.[0-9]+.[0-9]+-*' 8 | workflow_dispatch: 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: write 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Create build directory 21 | run: mkdir -p build 22 | 23 | - name: Build AE addon 24 | run: python util/build-ae.py 25 | 26 | - name: Build Blender addon 27 | run: python util/build-blender.py 28 | 29 | - name: Display structure of downloaded files 30 | run: ls -l 31 | working-directory: ./build 32 | 33 | - name: Create release 34 | uses: ncipollo/release-action@v1 35 | with: 36 | artifacts: "./build/*" 37 | draft: true 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-exporting-from-after-effects.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue exporting from After Effects 3 | about: There's a bug when exporting from After Effects to .json 4 | title: '' 5 | labels: bug, export 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **Error message** 14 | ``` 15 | Error message goes here 16 | ``` 17 | 18 | **To Reproduce** 19 | 26 | 27 | **Screenshots** 28 | 29 | 30 | **Test Case** 31 | 32 | 33 | **Platform Info** 34 | - 35 | - 36 | 37 | **Additional context** 38 | 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-importing-into-blender.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue importing into Blender 3 | about: There's a bug when importing .json files to Blender 4 | title: '' 5 | labels: bug, import 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **Error message** 14 | 16 | 17 | **To Reproduce** 18 | 25 | 26 | **Screenshots** 27 | 28 | 29 | **Test Case** 30 | 31 | 32 | **Platform Info** 33 | - 34 | - 35 | 36 | **Additional context** 37 | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/issue-in-import-export.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue in import/export 3 | about: There's a bug when exporting and importing, but it's unclear whether it's on 4 | the Blender or After Effects side 5 | title: '' 6 | labels: bug 7 | assignees: '' 8 | 9 | --- 10 | 11 | **Describe the bug** 12 | 13 | 14 | **To Reproduce** 15 | 22 | 23 | **Expected behavior** 24 | 25 | 26 | **Screenshots** 27 | 28 | 29 | **Test Case** 30 | 31 | 32 | **Platform Info** 33 | - 34 | - 35 | - 36 | 37 | **Additional context** 38 | 39 | -------------------------------------------------------------------------------- /util/build-ae.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Process all `@include` comments in the After Effects script, bundling everything into a single file. 3 | ''' 4 | 5 | import re 6 | from os import path 7 | 8 | incl_regex = re.compile('// ?@include *(\'([^\']+)\'|"([^"]+)")') 9 | 10 | def process_includes(file_path, root_file=False): 11 | srcdir = path.dirname(file_path) 12 | def make_include(matchobj): 13 | return process_includes(path.join(srcdir, matchobj.group(1)[1:-1])) 14 | 15 | contents = '' 16 | with open(file_path, encoding='utf-8') as file: 17 | contents = file.read() 18 | contents = incl_regex.sub(make_include, contents) 19 | if not root_file: 20 | return contents 21 | with open('LICENSE_JS', 'r', encoding='utf-8') as license: 22 | license_contents = license.read() 23 | return f'/*\n{license_contents}*/\n\n{contents}' 24 | 25 | 26 | with open('build/Export Composition Data to JSON.jsx', 'wb') as file: 27 | file.write(bytes(process_includes('export-comp-from-ae/export-comp-from-ae.jsx', True), 'UTF-8')) 28 | -------------------------------------------------------------------------------- /import-comp-to-blender/legacy_error.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This is a simple shim file to display an error message if you try to load this addon in a version of Blender that uses 3 | the old extension format. This file is ignored by 4.2 and up. 4 | 5 | Without this, those old Blender versions will happily say "module successfully installed!" without actually installing 6 | anything. 7 | ''' 8 | 9 | bl_info = { 10 | "name": "Import After Effects Composition", 11 | "description": "This add-on is incompatible with Blender versions below 4.2. Please update your Blender version.", 12 | "author": "adroitwhiz", 13 | "version": (0, 0, 0), 14 | "blender": (4, 2, 0), 15 | "category": "Import-Export", 16 | "doc_url": "https://github.com/adroitwhiz/after-effects-to-blender-export/", 17 | } 18 | 19 | def show_err(self, context): 20 | self.layout.label(text="This add-on is incompatible with Blender versions below 4.2. Please update your Blender version.") 21 | 22 | def register(): 23 | raise Exception("This add-on is incompatible with Blender versions below 4.2. Please update your Blender version.") 24 | 25 | 26 | def unregister(): 27 | pass 28 | 29 | if __name__ == "__main__": 30 | register() 31 | -------------------------------------------------------------------------------- /export-comp-from-ae/lib/util.js: -------------------------------------------------------------------------------- 1 | // A collection of helpful utility functions, (mostly) shamelessly stolen from Rhubarb Lip Sync 2 | // https://github.com/DanielSWolf/rhubarb-lip-sync 3 | // Their license is as follows: 4 | /* 5 | Copyright (c) 2015-2016 Daniel Wolf 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of 8 | this software and associated documentation files (the "Software"), to deal in 9 | the Software without restriction, including without limitation the rights to 10 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software is furnished to do so, 12 | subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 19 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 20 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 21 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 22 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | */ 24 | 25 | // Polyfill for Object.assign 26 | "function"!=typeof Object.assign&&(Object.assign=function(a,b){"use strict";if(null==a)throw new TypeError("Cannot convert undefined or null to object");for(var c=Object(a),d=1;d>>0;if("function"!=typeof r)throw new TypeError(r+" is not a function");for(arguments.length>1&&(t=arguments[1]),n=new Array(i),o=0;o>>0;if("function"!=typeof a)throw new TypeError(a+" is not a function");for(arguments.length>1&&(c=b),d=0;d>>0;if(0===i)return-1;var o=0|t;if(o>=i)return-1;for(n=Math.max(o>=0?o:i-Math.abs(o),0);n Scripts > Install Script File...: 10 | 11 | ![AE step 1](docs/ae-step1.png) 12 | 13 | You'll need to restart After Effects after installing it. 14 | 15 | Make sure that "Allow Scripts to Write Files and Access Network" is enabled: 16 | ![AE step 2](docs/ae-step2.png) 17 | 18 | To use the script, you must first make sure you're in the Composition view: 19 | 20 | ![AE step 3](docs/ae-step3.png) 21 | 22 | Then run the script: 23 | 24 | ![AE step 4](docs/ae-step4.png) 25 | 26 | ## After Effects Script Options 27 | 28 | When you run the script, a dialog box will appear: 29 | 30 | ![AE step 5](docs/ae-step5.png) 31 | 32 | The settings are as follows: 33 | 34 | #### Save to 35 | Choose the destination of the exported composition data file. This file can then be imported into Blender. 36 | 37 | #### Time range 38 | Some animated properties, and properties controlled via expressions, have keyframes that cannot be directly imported into Blender, and they must be "baked" in After Effects, meaning that the property's value is calculated once per frame. This setting controls the time range that the keyframes will be generated over. 39 | - "Whole comp": keyframes will be generated for the entire duration of the composition. 40 | - "Work area": keyframes will only be generated within the composition's work area. 41 | - "Layer duration": keyframes will only be generated within the duration(s) of the exported layer(s). 42 | 43 | #### Export selected layers only 44 | When checked, this ensures that only the layers you select will be exported. If "Bake transforms" is not checked, the selected layers' parents will be imported as well even if they aren't selected, in order to ensure that the child layers are properly transformed. 45 | 46 | #### Bake transforms 47 | When checked, all layer transforms will be "baked" in After Effects instead of being imported keyframes-and-all into Blender. In case of a bug in the importer, complicated scenarios (like a 3D layer parented to a 2D layer parented to a 3D layer), or unimplemented features (like Auto-Orient), this may be necessary. 48 | 49 | #### Transform sampling rate 50 | For those properties with keyframes that cannot be directly imported and must be "baked" (see above), this setting controls how many times they will be sampled per frame. The default setting of 1 is usually fine, but if there's some extremely fast motion (most common when simulating camera shake with a "wiggle" expression), and/or you want accurate motion blur trails, you can increase this. 51 | 52 | ## Installation / Usage (Blender) 53 | 54 | To install the Blender add-on, [download](https://github.com/adroitwhiz/after-effects-to-blender-export/releases), install, and then enable it via the add-on preferences: 55 | 56 | ![Blender step 1](docs/blender-step1.png) 57 | 58 | **DO NOT** download just the `__init.py__` file, or try to extract any files from the .zip! When selecting the addon to install from disk in Blender, choose the *full* .zip file. Otherwise, it will not work. 59 | 60 | To import camera data exported from After Effects, navigate to File > Import > After Effects composition data, converted (.json): 61 | 62 | ![Blender step 2](docs/blender-step2.png) 63 | 64 | There are several import options on the righthand pane of the file dialog: 65 | 66 | ![Blender step 3](docs/blender-step3.png) 67 | 68 | #### Scale Factor 69 | This is the factor by which all units in the imported composition will be scaled. The default value, 0.01, maps one pixel to one centimeter, which matches Cinema 4D. 70 | 71 | #### Handle FPS 72 | This determines how a composition with a different frame rate from the Blender scene's frame rate will be handled. The options are: 73 | - Preserve Frame Numbers: Keep the frame numbers the same, without changing the Blender scene's frame rate, if frame rates differ 74 | - Use Comp Frame Rate: Set the Blender scene's frame rate to match that of the imported composition 75 | - Remap Frame Times: If the Blender scene's frame rate differs, preserve the speed of the imported composition by changing frame numbers 76 | 77 | #### Comp Center to Origin 78 | If checked, this will position objects relative to Blender's origin rather than the composition center (which is down and to the right of Blender's origin). 79 | 80 | This matches the Cineware option for using a centered comp camera, and you should check this box if you select that option: 81 | 82 | ![Centered comp camera option](docs/ae-centered-comp-camera.png) 83 | 84 | #### Use Comp Resolution 85 | If checked, this will set the scene's render resolution to the resolution of the imported composition. 86 | 87 | #### Create New Collection 88 | If checked, this will place all imported objects into a new collection. 89 | 90 | #### Adjust Frame Start/End 91 | If checked, this will adjust the Start and End frames of the Blender scene's playback/rendering range to those of the imported composition's work area. 92 | 93 | #### Cameras to Markers 94 | If checked, this will create timeline markers and bind them to the imported camera layers' in/out points. This means that Blender will automatically switch between cameras the same way After Effects does. 95 | 96 | Once the desired options have been set, navigate to the .json file exported via the After Effects script, and click Import AE Comp: 97 | 98 | ![Blender step 4](docs/blender-step4.png) 99 | 100 | ## Development 101 | 102 | If a script file depends on other script files, After Effects' "Install Script File" option will not work. To get around this, I've created a preprocessor script that lives in `util/build-ae.py`. To use it, simply run it via Python: 103 | 104 | ```bash 105 | python3 util/build-ae.py 106 | ``` 107 | 108 | Or if you're on Windows: 109 | ```powershell 110 | py -3 util\build-ae.py 111 | ``` 112 | 113 | This will generate `Export Composition Data to JSON.jsx`. 114 | 115 | If you're working on the script, you don't need to re-preprocess the file every time you make a change--the `@include` directives are also recognized by After Effects itself. Simply run the script file located in `export-comp-from-ae` via File > Scripts > Run Script File. 116 | 117 | ## Roadmap 118 | 119 | These are in no particular order. If one of these features is helpful for your use case, open an issue and I can prioritize it. 120 | 121 | - Import options: 122 | - Toggle flip of Y/Z axes 123 | 124 | - After Effects features: 125 | - Nested 3D compositions 126 | - Images/videos as planes 127 | - Lights 128 | - Material options 129 | - Text layers 130 | - Shape layers 131 | - Opacity 132 | - Convert layer in/out points to Disable in Viewport/Render keyframes 133 | 134 | - Transforms: 135 | - 3D layer -> 2D layer -> 3D layer parent chain loses Z-transforming properties 136 | - Remove unanimated anchor points especially on null layers 137 | - Auto-Orient 138 | -------------------------------------------------------------------------------- /export-comp-from-ae/lib/json2.js: -------------------------------------------------------------------------------- 1 | // json2.js 2 | // 2017-06-12 3 | // Public Domain. 4 | // NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 5 | 6 | // USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 7 | // NOT CONTROL. 8 | 9 | // This file creates a global JSON object containing two methods: stringify 10 | // and parse. This file provides the ES5 JSON capability to ES3 systems. 11 | // If a project might run on IE8 or earlier, then this file should be included. 12 | // This file does nothing on ES5 systems. 13 | 14 | // JSON.stringify(value, replacer, space) 15 | // value any JavaScript value, usually an object or array. 16 | // replacer an optional parameter that determines how object 17 | // values are stringified for objects. It can be a 18 | // function or an array of strings. 19 | // space an optional parameter that specifies the indentation 20 | // of nested structures. If it is omitted, the text will 21 | // be packed without extra whitespace. If it is a number, 22 | // it will specify the number of spaces to indent at each 23 | // level. If it is a string (such as "\t" or " "), 24 | // it contains the characters used to indent at each level. 25 | // This method produces a JSON text from a JavaScript value. 26 | // When an object value is found, if the object contains a toJSON 27 | // method, its toJSON method will be called and the result will be 28 | // stringified. A toJSON method does not serialize: it returns the 29 | // value represented by the name/value pair that should be serialized, 30 | // or undefined if nothing should be serialized. The toJSON method 31 | // will be passed the key associated with the value, and this will be 32 | // bound to the value. 33 | 34 | // For example, this would serialize Dates as ISO strings. 35 | 36 | // Date.prototype.toJSON = function (key) { 37 | // function f(n) { 38 | // // Format integers to have at least two digits. 39 | // return (n < 10) 40 | // ? "0" + n 41 | // : n; 42 | // } 43 | // return this.getUTCFullYear() + "-" + 44 | // f(this.getUTCMonth() + 1) + "-" + 45 | // f(this.getUTCDate()) + "T" + 46 | // f(this.getUTCHours()) + ":" + 47 | // f(this.getUTCMinutes()) + ":" + 48 | // f(this.getUTCSeconds()) + "Z"; 49 | // }; 50 | 51 | // You can provide an optional replacer method. It will be passed the 52 | // key and value of each member, with this bound to the containing 53 | // object. The value that is returned from your method will be 54 | // serialized. If your method returns undefined, then the member will 55 | // be excluded from the serialization. 56 | 57 | // If the replacer parameter is an array of strings, then it will be 58 | // used to select the members to be serialized. It filters the results 59 | // such that only members with keys listed in the replacer array are 60 | // stringified. 61 | 62 | // Values that do not have JSON representations, such as undefined or 63 | // functions, will not be serialized. Such values in objects will be 64 | // dropped; in arrays they will be replaced with null. You can use 65 | // a replacer function to replace those with JSON values. 66 | 67 | // JSON.stringify(undefined) returns undefined. 68 | 69 | // The optional space parameter produces a stringification of the 70 | // value that is filled with line breaks and indentation to make it 71 | // easier to read. 72 | 73 | // If the space parameter is a non-empty string, then that string will 74 | // be used for indentation. If the space parameter is a number, then 75 | // the indentation will be that many spaces. 76 | 77 | // Example: 78 | 79 | // text = JSON.stringify(["e", {pluribus: "unum"}]); 80 | // // text is '["e",{"pluribus":"unum"}]' 81 | 82 | // text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t"); 83 | // // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 84 | 85 | // text = JSON.stringify([new Date()], function (key, value) { 86 | // return this[key] instanceof Date 87 | // ? "Date(" + this[key] + ")" 88 | // : value; 89 | // }); 90 | // // text is '["Date(---current time---)"]' 91 | 92 | // JSON.parse(text, reviver) 93 | // This method parses a JSON text to produce an object or array. 94 | // It can throw a SyntaxError exception. 95 | 96 | // The optional reviver parameter is a function that can filter and 97 | // transform the results. It receives each of the keys and values, 98 | // and its return value is used instead of the original value. 99 | // If it returns what it received, then the structure is not modified. 100 | // If it returns undefined then the member is deleted. 101 | 102 | // Example: 103 | 104 | // // Parse the text. Values that look like ISO date strings will 105 | // // be converted to Date objects. 106 | 107 | // myData = JSON.parse(text, function (key, value) { 108 | // var a; 109 | // if (typeof value === "string") { 110 | // a = 111 | // /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 112 | // if (a) { 113 | // return new Date(Date.UTC( 114 | // +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6] 115 | // )); 116 | // } 117 | // return value; 118 | // } 119 | // }); 120 | 121 | // myData = JSON.parse( 122 | // "[\"Date(09/09/2001)\"]", 123 | // function (key, value) { 124 | // var d; 125 | // if ( 126 | // typeof value === "string" 127 | // && value.slice(0, 5) === "Date(" 128 | // && value.slice(-1) === ")" 129 | // ) { 130 | // d = new Date(value.slice(5, -1)); 131 | // if (d) { 132 | // return d; 133 | // } 134 | // } 135 | // return value; 136 | // } 137 | // ); 138 | 139 | // This is a reference implementation. You are free to copy, modify, or 140 | // redistribute. 141 | 142 | /*jslint 143 | eval, for, this 144 | */ 145 | 146 | /*property 147 | JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 148 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 149 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 150 | test, toJSON, toString, valueOf 151 | */ 152 | 153 | 154 | // Create a JSON object only if one does not already exist. We create the 155 | // methods in a closure to avoid creating global variables. 156 | 157 | if (typeof JSON !== "object") { 158 | JSON = {}; 159 | } 160 | 161 | (function () { 162 | "use strict"; 163 | 164 | var rx_one = /^[\],:{}\s]*$/; 165 | var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g; 166 | var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g; 167 | var rx_four = /(?:^|:|,)(?:\s*\[)+/g; 168 | var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 169 | var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g; 170 | 171 | function f(n) { 172 | // Format integers to have at least two digits. 173 | return (n < 10) 174 | ? "0" + n 175 | : n; 176 | } 177 | 178 | function this_value() { 179 | return this.valueOf(); 180 | } 181 | 182 | if (typeof Date.prototype.toJSON !== "function") { 183 | 184 | Date.prototype.toJSON = function () { 185 | 186 | return isFinite(this.valueOf()) 187 | ? ( 188 | this.getUTCFullYear() 189 | + "-" 190 | + f(this.getUTCMonth() + 1) 191 | + "-" 192 | + f(this.getUTCDate()) 193 | + "T" 194 | + f(this.getUTCHours()) 195 | + ":" 196 | + f(this.getUTCMinutes()) 197 | + ":" 198 | + f(this.getUTCSeconds()) 199 | + "Z" 200 | ) 201 | : null; 202 | }; 203 | 204 | Boolean.prototype.toJSON = this_value; 205 | Number.prototype.toJSON = this_value; 206 | String.prototype.toJSON = this_value; 207 | } 208 | 209 | var gap; 210 | var indent; 211 | var meta; 212 | var rep; 213 | 214 | 215 | function quote(string) { 216 | 217 | // If the string contains no control characters, no quote characters, and no 218 | // backslash characters, then we can safely slap some quotes around it. 219 | // Otherwise we must also replace the offending characters with safe escape 220 | // sequences. 221 | 222 | rx_escapable.lastIndex = 0; 223 | return rx_escapable.test(string) 224 | ? "\"" + string.replace(rx_escapable, function (a) { 225 | var c = meta[a]; 226 | return typeof c === "string" 227 | ? c 228 | : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4); 229 | }) + "\"" 230 | : "\"" + string + "\""; 231 | } 232 | 233 | 234 | function str(key, holder) { 235 | 236 | // Produce a string from holder[key]. 237 | 238 | var i; // The loop counter. 239 | var k; // The member key. 240 | var v; // The member value. 241 | var length; 242 | var mind = gap; 243 | var partial; 244 | var value = holder[key]; 245 | 246 | // If the value has a toJSON method, call it to obtain a replacement value. 247 | 248 | if ( 249 | value 250 | && typeof value === "object" 251 | && typeof value.toJSON === "function" 252 | ) { 253 | value = value.toJSON(key); 254 | } 255 | 256 | // If we were called with a replacer function, then call the replacer to 257 | // obtain a replacement value. 258 | 259 | if (typeof rep === "function") { 260 | value = rep.call(holder, key, value); 261 | } 262 | 263 | // What happens next depends on the value's type. 264 | 265 | switch (typeof value) { 266 | case "string": 267 | return quote(value); 268 | 269 | case "number": 270 | 271 | // JSON numbers must be finite. Encode non-finite numbers as null. 272 | 273 | return (isFinite(value)) 274 | ? String(value) 275 | : "null"; 276 | 277 | case "boolean": 278 | case "null": 279 | 280 | // If the value is a boolean or null, convert it to a string. Note: 281 | // typeof null does not produce "null". The case is included here in 282 | // the remote chance that this gets fixed someday. 283 | 284 | return String(value); 285 | 286 | // If the type is "object", we might be dealing with an object or an array or 287 | // null. 288 | 289 | case "object": 290 | 291 | // Due to a specification blunder in ECMAScript, typeof null is "object", 292 | // so watch out for that case. 293 | 294 | if (!value) { 295 | return "null"; 296 | } 297 | 298 | // Make an array to hold the partial results of stringifying this object value. 299 | 300 | gap += indent; 301 | partial = []; 302 | 303 | // Is the value an array? 304 | 305 | if (Object.prototype.toString.apply(value) === "[object Array]") { 306 | 307 | // The value is an array. Stringify every element. Use null as a placeholder 308 | // for non-JSON values. 309 | 310 | length = value.length; 311 | for (i = 0; i < length; i += 1) { 312 | partial[i] = str(i, value) || "null"; 313 | } 314 | 315 | // Join all of the elements together, separated with commas, and wrap them in 316 | // brackets. 317 | 318 | v = partial.length === 0 319 | ? "[]" 320 | : gap 321 | ? ( 322 | "[\n" 323 | + gap 324 | + partial.join(",\n" + gap) 325 | + "\n" 326 | + mind 327 | + "]" 328 | ) 329 | : "[" + partial.join(",") + "]"; 330 | gap = mind; 331 | return v; 332 | } 333 | 334 | // If the replacer is an array, use it to select the members to be stringified. 335 | 336 | if (rep && typeof rep === "object") { 337 | length = rep.length; 338 | for (i = 0; i < length; i += 1) { 339 | if (typeof rep[i] === "string") { 340 | k = rep[i]; 341 | v = str(k, value); 342 | if (v) { 343 | partial.push(quote(k) + ( 344 | (gap) 345 | ? ": " 346 | : ":" 347 | ) + v); 348 | } 349 | } 350 | } 351 | } else { 352 | 353 | // Otherwise, iterate through all of the keys in the object. 354 | 355 | for (k in value) { 356 | if (Object.prototype.hasOwnProperty.call(value, k)) { 357 | v = str(k, value); 358 | if (v) { 359 | partial.push(quote(k) + ( 360 | (gap) 361 | ? ": " 362 | : ":" 363 | ) + v); 364 | } 365 | } 366 | } 367 | } 368 | 369 | // Join all of the member texts together, separated with commas, 370 | // and wrap them in braces. 371 | 372 | v = partial.length === 0 373 | ? "{}" 374 | : gap 375 | ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}" 376 | : "{" + partial.join(",") + "}"; 377 | gap = mind; 378 | return v; 379 | } 380 | } 381 | 382 | // If the JSON object does not yet have a stringify method, give it one. 383 | 384 | if (typeof JSON.stringify !== "function") { 385 | meta = { // table of character substitutions 386 | "\b": "\\b", 387 | "\t": "\\t", 388 | "\n": "\\n", 389 | "\f": "\\f", 390 | "\r": "\\r", 391 | "\"": "\\\"", 392 | "\\": "\\\\" 393 | }; 394 | JSON.stringify = function (value, replacer, space) { 395 | 396 | // The stringify method takes a value and an optional replacer, and an optional 397 | // space parameter, and returns a JSON text. The replacer can be a function 398 | // that can replace values, or an array of strings that will select the keys. 399 | // A default replacer method can be provided. Use of the space parameter can 400 | // produce text that is more easily readable. 401 | 402 | var i; 403 | gap = ""; 404 | indent = ""; 405 | 406 | // If the space parameter is a number, make an indent string containing that 407 | // many spaces. 408 | 409 | if (typeof space === "number") { 410 | for (i = 0; i < space; i += 1) { 411 | indent += " "; 412 | } 413 | 414 | // If the space parameter is a string, it will be used as the indent string. 415 | 416 | } else if (typeof space === "string") { 417 | indent = space; 418 | } 419 | 420 | // If there is a replacer, it must be a function or an array. 421 | // Otherwise, throw an error. 422 | 423 | rep = replacer; 424 | if (replacer && typeof replacer !== "function" && ( 425 | typeof replacer !== "object" 426 | || typeof replacer.length !== "number" 427 | )) { 428 | throw new Error("JSON.stringify"); 429 | } 430 | 431 | // Make a fake root object containing our value under the key of "". 432 | // Return the result of stringifying the value. 433 | 434 | return str("", {"": value}); 435 | }; 436 | } 437 | 438 | 439 | // If the JSON object does not yet have a parse method, give it one. 440 | 441 | if (typeof JSON.parse !== "function") { 442 | JSON.parse = function (text, reviver) { 443 | 444 | // The parse method takes a text and an optional reviver function, and returns 445 | // a JavaScript value if the text is a valid JSON text. 446 | 447 | var j; 448 | 449 | function walk(holder, key) { 450 | 451 | // The walk method is used to recursively walk the resulting structure so 452 | // that modifications can be made. 453 | 454 | var k; 455 | var v; 456 | var value = holder[key]; 457 | if (value && typeof value === "object") { 458 | for (k in value) { 459 | if (Object.prototype.hasOwnProperty.call(value, k)) { 460 | v = walk(value, k); 461 | if (v !== undefined) { 462 | value[k] = v; 463 | } else { 464 | delete value[k]; 465 | } 466 | } 467 | } 468 | } 469 | return reviver.call(holder, key, value); 470 | } 471 | 472 | 473 | // Parsing happens in four stages. In the first stage, we replace certain 474 | // Unicode characters with escape sequences. JavaScript handles many characters 475 | // incorrectly, either silently deleting them, or treating them as line endings. 476 | 477 | text = String(text); 478 | rx_dangerous.lastIndex = 0; 479 | if (rx_dangerous.test(text)) { 480 | text = text.replace(rx_dangerous, function (a) { 481 | return ( 482 | "\\u" 483 | + ("0000" + a.charCodeAt(0).toString(16)).slice(-4) 484 | ); 485 | }); 486 | } 487 | 488 | // In the second stage, we run the text against regular expressions that look 489 | // for non-JSON patterns. We are especially concerned with "()" and "new" 490 | // because they can cause invocation, and "=" because it can cause mutation. 491 | // But just to be safe, we want to reject all unexpected forms. 492 | 493 | // We split the second stage into 4 regexp operations in order to work around 494 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 495 | // replace the JSON backslash pairs with "@" (a non-JSON character). Second, we 496 | // replace all simple value tokens with "]" characters. Third, we delete all 497 | // open brackets that follow a colon or comma or that begin the text. Finally, 498 | // we look to see that the remaining characters are only whitespace or "]" or 499 | // "," or ":" or "{" or "}". If that is so, then the text is safe for eval. 500 | 501 | if ( 502 | rx_one.test( 503 | text 504 | .replace(rx_two, "@") 505 | .replace(rx_three, "]") 506 | .replace(rx_four, "") 507 | ) 508 | ) { 509 | 510 | // In the third stage we use the eval function to compile the text into a 511 | // JavaScript structure. The "{" operator is subject to a syntactic ambiguity 512 | // in JavaScript: it can begin a block or an object literal. We wrap the text 513 | // in parens to eliminate the ambiguity. 514 | 515 | j = eval("(" + text + ")"); 516 | 517 | // In the optional fourth stage, we recursively walk the new structure, passing 518 | // each name/value pair to a reviver function for possible transformation. 519 | 520 | return (typeof reviver === "function") 521 | ? walk({"": j}, "") 522 | : j; 523 | } 524 | 525 | // If the text is not JSON parseable, then a SyntaxError is thrown. 526 | 527 | throw new SyntaxError("JSON.parse"); 528 | }; 529 | } 530 | }()); 531 | -------------------------------------------------------------------------------- /LICENSE_JS: -------------------------------------------------------------------------------- 1 | Copyright © 2020-2025 adroitwhiz 2 | 3 | Common Public Attribution License Version 1.0 (CPAL) 4 | 5 | 1. “Definitions” 6 | 7 | 1.0.1 “Commercial Use” means distribution or otherwise making the Covered Code available to a third party. 8 | 9 | 1.1 “Contributor” means each entity that creates or contributes to the creation of Modifications. 10 | 11 | 1.2 “Contributor Version” means the combination of the Original Code, prior Modifications used by a Contributor, and the Modifications made by that particular Contributor. 12 | 13 | 1.3 “Covered Code” means the Original Code or Modifications or the combination of the Original Code and Modifications, in each case including portions thereof. 14 | 15 | 1.4 “Electronic Distribution Mechanism” means a mechanism generally accepted in the software development community for the electronic transfer of data. 16 | 17 | 1.5 “Executable” means Covered Code in any form other than Source Code. 18 | 19 | 1.6 “Initial Developer” means the individual or entity identified as the Initial Developer in the Source Code notice required by Exhibit A. 20 | 21 | 1.7 “Larger Work” means a work which combines Covered Code or portions thereof with code not governed by the terms of this License. 22 | 23 | 1.8 “License” means this document. 24 | 25 | 1.8.1 “Licensable” means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein. 26 | 27 | 1.9 “Modifications” means any addition to or deletion from the substance or structure of either the Original Code or any previous Modifications. When Covered Code is released as a series of files, a Modification is: 28 | 29 | A. Any addition to or deletion from the contents of a file containing Original Code or previous Modifications. 30 | 31 | B. Any new file that contains any part of the Original Code or previous Modifications. 32 | 33 | 1.10 “Original Code” means Source Code of computer software code which is described in the Source Code notice required by Exhibit A as Original Code, and which, at the time of its release under this License is not already Covered Code governed by this License. 34 | 35 | 1.10.1 “Patent Claims” means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor. 36 | 37 | 1.11 “Source Code” means the preferred form of the Covered Code for making modifications to it, including all modules it contains, plus any associated interface definition files, scripts used to control compilation and installation of an Executable, or source code differential comparisons against either the Original Code or another well known, available Covered Code of the Contributor’s choice. The Source Code can be in a compressed or archival form, provided the appropriate decompression or de-archiving software is widely available for no charge. 38 | 39 | 1.12 “You” (or “Your”) means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License or a future version of this License issued under Section 6.1. For legal entities, “You” includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, “control” means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 40 | 41 | 2. Source Code License. 42 | 43 | 2.1 The Initial Developer Grant. 44 | The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims: 45 | 46 | (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer to use, reproduce, modify, display, perform, sublicense and distribute the Original Code (or portions thereof) with or without Modifications, and/or as part of a Larger Work; and 47 | 48 | (b) under Patents Claims infringed by the making, using or selling of Original Code, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Code (or portions thereof). 49 | 50 | (c) the licenses granted in this Section 2.1(a) and (b) are effective on the date Initial Developer first distributes Original Code under the terms of this License. 51 | 52 | (d) Notwithstanding Section 2.1(b) above, no patent license is granted: 1) for code that You delete from the Original Code; 2) separate from the Original Code; or 3) for infringements caused by: i) the modification of the Original Code or ii) the combination of the Original Code with other software or devices. 53 | 54 | 2.2 Contributor Grant. 55 | Subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license 56 | 57 | (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor, to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof) either on an unmodified basis, with other Modifications, as Covered Code and/or as part of a Larger Work; and 58 | 59 | (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: 1) Modifications made by that Contributor (or portions thereof); and 2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination). 60 | 61 | (c) the licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first makes Commercial Use of the Covered Code. 62 | 63 | (d) Notwithstanding Section 2.2(b) above, no patent license is granted: 1) for any code that Contributor has deleted from the Contributor Version; 2) separate from the Contributor Version; 3) for infringements caused by: i) third party modifications of Contributor Version or ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or 4) under Patent Claims infringed by Covered Code in the absence of Modifications made by that Contributor. 64 | 65 | 3. Distribution Obligations. 66 | 67 | 3.1 Application of License. 68 | The Modifications which You create or to which You contribute are governed by the terms of this License, including without limitation Section 2.2. The Source Code version of Covered Code may be distributed only under the terms of this License or a future version of this License released under Section 6.1, and You must include a copy of this License with every copy of the Source Code You distribute. You may not offer or impose any terms on any Source Code version that alters or restricts the applicable version of this License or the recipients’ rights hereunder. However, You may include an additional document offering the additional rights described in Section 3.5. 69 | 70 | 3.2 Availability of Source Code. 71 | Any Modification which You create or to which You contribute must be made available in Source Code form under the terms of this License either on the same media as an Executable version or via an accepted Electronic Distribution Mechanism to anyone to whom you made an Executable version available; and if made available via Electronic Distribution Mechanism, must remain available for at least twelve (12) months after the date it initially became available, or at least six (6) months after a subsequent version of that particular Modification has been made available to such recipients. You are responsible for ensuring that the Source Code version remains available even if the Electronic Distribution Mechanism is maintained by a third party. 72 | 73 | 3.3 Description of Modifications. 74 | You must cause all Covered Code to which You contribute to contain a file documenting the changes You made to create that Covered Code and the date of any change. You must include a prominent statement that the Modification is derived, directly or indirectly, from Original Code provided by the Initial Developer and including the name of the Initial Developer in (a) the Source Code, and (b) in any notice in an Executable version or related documentation in which You describe the origin or ownership of the Covered Code. 75 | 76 | 3.4 Intellectual Property Matters 77 | 78 | (a) Third Party Claims. 79 | If Contributor has knowledge that a license under a third party’s intellectual property rights is required to exercise the rights granted by such Contributor under Sections 2.1 or 2.2, Contributor must include a text file with the Source Code distribution titled “LEGAL” which describes the claim and the party making the claim in sufficient detail that a recipient will know whom to contact. If Contributor obtains such knowledge after the Modification is made available as described in Section 3.2, Contributor shall promptly modify the LEGAL file in all copies Contributor makes available thereafter and shall take other steps (such as notifying appropriate mailing lists or newsgroups) reasonably calculated to inform those who received the Covered Code that new knowledge has been obtained. 80 | 81 | (b) Contributor APIs. 82 | If Contributor’s Modifications include an application programming interface and Contributor has knowledge of patent licenses which are reasonably necessary to implement that API, Contributor must also include this information in the LEGAL file. 83 | 84 | (c) Representations. 85 | Contributor represents that, except as disclosed pursuant to Section 3.4(a) above, Contributor believes that Contributor’s Modifications are Contributor’s original creation(s) and/or Contributor has sufficient rights to grant the rights conveyed by this License. 86 | 87 | 3.5 Required Notices. 88 | You must duplicate the notice in Exhibit A in each file of the Source Code. If it is not possible to put such notice in a particular Source Code file due to its structure, then You must include such notice in a location (such as a relevant directory) where a user would be likely to look for such a notice. If You created one or more Modification(s) You may add your name as a Contributor to the notice described in Exhibit A. You must also duplicate this License in any documentation for the Source Code where You describe recipients’ rights or ownership rights relating to Covered Code. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Code. However, You may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear than any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer. 89 | 90 | 3.6 Distribution of Executable Versions. 91 | You may distribute Covered Code in Executable form only if the requirements of Section 3.1-3.5 have been met for that Covered Code, and if You include a notice stating that the Source Code version of the Covered Code is available under the terms of this License, including a description of how and where You have fulfilled the obligations of Section 3.2. The notice must be conspicuously included in any notice in an Executable version, related documentation or collateral in which You describe recipients’ rights relating to the Covered Code. You may distribute the Executable version of Covered Code or ownership rights under a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable version does not attempt to limit or alter the recipient’s rights in the Source Code version from the rights set forth in this License. If You distribute the Executable version under a different license You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer, Original Developer or any Contributor. You hereby agree to indemnify the Initial Developer, Original Developer and every Contributor for any liability incurred by the Initial Developer, Original Developer or such Contributor as a result of any such terms You offer. 92 | 93 | 3.7 Larger Works. 94 | You may create a Larger Work by combining Covered Code with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Code. 95 | 96 | 4. Inability to Comply Due to Statute or Regulation. 97 | 98 | If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Code due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be included in the LEGAL file described in Section 3.4 and must be included with all distributions of the Source Code. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 99 | 100 | 5. Application of this License. 101 | 102 | This License applies to code to which the Initial Developer has attached the notice in Exhibit A and to related Covered Code. 103 | 104 | 6. Versions of the License. 105 | 106 | 6.1 New Versions. 107 | Socialtext, Inc. (“Socialtext”) may publish revised and/or new versions of the License from time to time. Each version will be given a distinguishing version number. 108 | 109 | 6.2 Effect of New Versions. 110 | Once Covered Code has been published under a particular version of the License, You may always continue to use it under the terms of that version. You may also choose to use such Covered Code under the terms of any subsequent version of the License published by Socialtext. No one other than Socialtext has the right to modify the terms applicable to Covered Code created under this License. 111 | 112 | 6.3 Derivative Works. 113 | If You create or use a modified version of this License (which you may only do in order to apply it to code which is not already Covered Code governed by this License), You must (a) rename Your license so that the phrases “Socialtext”, “CPAL” or any confusingly similar phrase do not appear in your license (except to note that your license differs from this License) and (b) otherwise make it clear that Your version of the license contains terms which differ from the CPAL. (Filling in the name of the Initial Developer, Original Developer, Original Code or Contributor in the notice described in Exhibit A shall not of themselves be deemed to be modifications of this License.) 114 | 115 | 7. DISCLAIMER OF WARRANTY. 116 | 117 | COVERED CODE IS PROVIDED UNDER THIS LICENSE ON AN “AS IS” BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED CODE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED CODE IS WITH YOU. SHOULD ANY COVERED CODE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER, ORIGINAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED CODE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER. 118 | 119 | 8. TERMINATION. 120 | 121 | 8.1 This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. All sublicenses to the Covered Code which are properly granted shall survive any termination of this License. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive. 122 | 123 | 8.2 If You initiate litigation by asserting a patent infringement claim (excluding declatory judgment actions) against Initial Developer, Original Developer or a Contributor (the Initial Developer, Original Developer or Contributor against whom You file such action is referred to as “Participant”) alleging that: 124 | 125 | (a) such Participant’s Contributor Version directly or indirectly infringes any patent, then any and all rights granted by such Participant to You under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively, unless if within 60 days after receipt of notice You either: (i) agree in writing to pay Participant a mutually agreeable reasonable royalty for Your past and future use of Modifications made by such Participant, or (ii) withdraw Your litigation claim with respect to the Contributor Version against such Participant. If within 60 days of notice, a reasonable royalty and payment arrangement are not mutually agreed upon in writing by the parties or the litigation claim is not withdrawn, the rights granted by Participant to You under Sections 2.1 and/or 2.2 automatically terminate at the expiration of the 60 day notice period specified above. 126 | 127 | (b) any software, hardware, or device, other than such Participant’s Contributor Version, directly or indirectly infringes any patent, then any rights granted to You by such Participant under Sections 2.1(b) and 2.2(b) are revoked effective as of the date You first made, used, sold, distributed, or had made, Modifications made by that Participant. 128 | 129 | 8.3 If You assert a patent infringement claim against Participant alleging that such Participant’s Contributor Version directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be taken into account in determining the amount or value of any payment or license. 130 | 131 | 8.4 In the event of termination under Sections 8.1 or 8.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or any distributor hereunder prior to termination shall survive termination. 132 | 133 | 9. LIMITATION OF LIABILITY. 134 | 135 | UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ORIGINAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED CODE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY’S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU. 136 | 137 | 10. U.S. GOVERNMENT END USERS. 138 | 139 | The Covered Code is a “commercial item,” as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of “commercial computer software” and “commercial computer software documentation,” as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Code with only those rights set forth herein. 140 | 141 | 11. MISCELLANEOUS. 142 | 143 | This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by California law provisions (except to the extent applicable law, if any, provides otherwise), excluding its conflict-of-law provisions. With respect to disputes in which at least one party is a citizen of, or an entity chartered or registered to do business in the United States of America, any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California, with venue lying in Santa Clara County, California, with the losing party responsible for costs, including without limitation, court costs and reasonable attorneys’ fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. 144 | 145 | 12. RESPONSIBILITY FOR CLAIMS. 146 | 147 | As between Initial Developer, Original Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer, Original Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability. 148 | 149 | 13. MULTIPLE-LICENSED CODE. 150 | 151 | Initial Developer may designate portions of the Covered Code as Multiple-Licensed. Multiple-Licensed means that the Initial Developer permits you to utilize portions of the Covered Code under Your choice of the CPAL or the alternative licenses, if any, specified by the Initial Developer in the file described in Exhibit A. 152 | 153 | 14. ADDITIONAL TERM: ATTRIBUTION 154 | 155 | (a) As a modest attribution to the organizer of the development of the Original Code (“Original Developer”), in the hope that its promotional value may help justify the time, money and effort invested in writing the Original Code, the Original Developer may include in Exhibit B (“Attribution Information”) a requirement that each time an Executable and Source Code or a Larger Work is launched or initially run (which includes initiating a session), a prominent display of the Original Developer’s Attribution Information (as defined below) must occur on the graphic user interface employed by the end user to access such Covered Code (which may include display on a splash screen), if any. The size of the graphic image should be consistent with the size of the other elements of the Attribution Information. If the access by the end user to the Executable and Source Code does not create a graphic user interface for access to the Covered Code, this obligation shall not apply. If the Original Code displays such Attribution Information in a particular form (such as in the form of a splash screen, notice at login, an “about” display, or dedicated attribution area on user interface screens), continued use of such form for that Attribution Information is one way of meeting this requirement for notice. 156 | 157 | (b) Attribution information may only include a copyright notice, a brief phrase, graphic image and a URL (“Attribution Information”) and is subject to the Attribution Limits as defined below. For these purposes, prominent shall mean display for sufficient duration to give reasonable notice to the user of the identity of the Original Developer and that if You include Attribution Information or similar information for other parties, You must ensure that the Attribution Information for the Original Developer shall be no less prominent than such Attribution Information or similar information for the other party. For greater certainty, the Original Developer may choose to specify in Exhibit B below that the above attribution requirement only applies to an Executable and Source Code resulting from the Original Code or any Modification, but not a Larger Work. The intent is to provide for reasonably modest attribution, therefore the Original Developer cannot require that You display, at any time, more than the following information as Attribution Information: (a) a copyright notice including the name of the Original Developer; (b) a word or one phrase (not exceeding 10 words); (c) one graphic image provided by the Original Developer; and (d) a URL (collectively, the “Attribution Limits”). 158 | 159 | (c) If Exhibit B does not include any Attribution Information, then there are no requirements for You to display any Attribution Information of the Original Developer. 160 | 161 | (d) You acknowledge that all trademarks, service marks and/or trade names contained within the Attribution Information distributed with the Covered Code are the exclusive property of their owners and may only be used with the permission of their owners, or under circumstances otherwise permitted by law or as expressly set out in this License. 162 | 163 | 15. ADDITIONAL TERM: NETWORK USE. 164 | 165 | The term “External Deployment” means the use, distribution, or communication of the Original Code or Modifications in any way such that the Original Code or Modifications may be used by anyone other than You, whether those works are distributed or communicated to those persons or made available as an application intended for use over a network. As an express condition for the grants of license hereunder, You must treat any External Deployment by You of the Original Code or Modifications as a distribution under section 3.1 and make Source Code available under Section 3.2. 166 | 167 | EXHIBIT A. Common Public Attribution License Version 1.0. 168 | 169 | “The contents of this file are subject to the Common Public Attribution License Version 1.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at https://opensource.org/license/cpal_1.0. The License is based on the Mozilla Public License Version 1.1 but Sections 14 and 15 have been added to cover use of software over a computer network and provide for limited attribution for the Original Developer. In addition, Exhibit A has been modified to be consistent with Exhibit B. 170 | 171 | Software distributed under the License is distributed on an “AS IS” basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the specific language governing rights and limitations under the License. 172 | 173 | The Original Code is after-effects-to-blender-export. 174 | 175 | The Original Developer is the Initial Developer. 176 | 177 | The Initial Developer of the Original Code is adroitwhiz. All portions of the code written by adroitwhiz are Copyright (c) adroitwhiz. All Rights Reserved. 178 | 179 | EXHIBIT B. Attribution Information 180 | 181 | Attribution Copyright Notice: Copyright © 2020-2025 adroitwhiz. All Rights Reserved. 182 | 183 | Attribution URL: https://github.com/adroitwhiz/after-effects-to-blender-export/ 184 | 185 | Display of Attribution Information is required in Larger Works which are defined in the CPAL as a work which combines Covered Code or portions thereof with code not governed by the terms of the CPAL. 186 | -------------------------------------------------------------------------------- /export-comp-from-ae/export-comp-from-ae.jsx: -------------------------------------------------------------------------------- 1 | { 2 | // @include 'lib/util.js' 3 | 4 | var fileVersion = 3; 5 | var settingsVersion = '0.2'; 6 | var settingsFilePath = Folder.userData.fullName + '/cam-export-settings.json'; 7 | 8 | function showDialog(cb, opts) { 9 | var c = controlFunctions; 10 | var resourceString = createResourceString( 11 | c.Dialog({ 12 | text: 'AE to Blender Export', 13 | settings: c.Group({ 14 | orientation: 'column', 15 | alignment: ['fill', 'fill'], 16 | alignChildren: ['left', 'top'], 17 | saveDestination: c.Group({ 18 | alignment: ['fill', 'fill'], 19 | label: c.StaticText({text: 'Save to'}), 20 | savePath: c.EditText({ 21 | alignment: ['fill', 'fill'], 22 | minimumSize: [400, 0] 23 | }), 24 | saveBrowse: c.Button({ 25 | properties: {name: 'browse'}, 26 | text: 'Browse', 27 | alignment: ['right', 'right'] 28 | }) 29 | }), 30 | timeRange: c.Group({ 31 | label: c.StaticText({text: 'Time range'}), 32 | value: c.Group({ 33 | wholeComp: c.RadioButton({ 34 | text: 'Whole comp', 35 | value: true 36 | }), 37 | workArea: c.RadioButton({ 38 | text: 'Work area' 39 | }), 40 | layerDuration: c.RadioButton({ 41 | text: 'Layer duration' 42 | }), 43 | // TODO: automatically detect per layer and channel whether to animate and when 44 | /*automatic: c.RadioButton({ 45 | text: 'Automatic' 46 | })*/ 47 | }) 48 | }), 49 | selectedLayersOnly: c.Group({ 50 | label: c.StaticText({ 51 | text: 'Export selected layers only' 52 | }), 53 | value: c.Checkbox({ 54 | value: opts.selectionExists, 55 | enabled: opts.selectionExists 56 | }) 57 | }), 58 | bakeTransforms: c.Group({ 59 | label: c.StaticText({ 60 | text: 'Bake transforms', 61 | helpTip: "Calculate all transforms in After Effects. This may help if Blender is importing some transforms incorrectly." 62 | }), 63 | value: c.Checkbox({ 64 | value: opts.bakeTransforms, 65 | enabled: opts.bakeTransforms 66 | }) 67 | }), 68 | frameSuperSampling: c.Group({ 69 | label: c.StaticText({ 70 | text: 'Transform sampling rate', 71 | helpTip: 'Number of keyframes to generate per frame, for baked transforms and those which cannot be exported directly. Useful for e.g. very fast "wiggle" expressions where you want to capture all the motion blur.' 72 | }), 73 | value: c.EditText({ 74 | text: opts.frameSuperSampling, 75 | minimumSize: [40, 0] 76 | }) 77 | }) 78 | }), 79 | separator: c.Group({ preferredSize: ['', 3] }), 80 | buttons: c.Group({ 81 | alignment: 'fill', 82 | plug: c.Group({ 83 | text: c.StaticText({ 84 | text: 'Copyright © 2020-2025 adroitwhiz. All Rights Reserved.', 85 | alignment: ['fill', 'center'] 86 | }), 87 | link: c.Button({ 88 | text: '↗', 89 | helpTip: 'Visit homepage', 90 | maximumSize: [20, 20] 91 | }) 92 | }), 93 | doExport: c.Button({ 94 | properties: { name: 'ok' }, 95 | text: 'Export', 96 | alignment: ['right', 'center'] 97 | }), 98 | cancel: c.Button({ 99 | properties: { name: 'cancel' }, 100 | text: 'Cancel', 101 | alignment: ['right', 'center'] 102 | }) 103 | }) 104 | }) 105 | ); 106 | 107 | var window = new Window(resourceString, 'Blender Export', undefined, {resizeable: false}); 108 | 109 | var controls = { 110 | savePath: window.settings.saveDestination.savePath, 111 | saveBrowse: window.settings.saveDestination.saveBrowse, 112 | timeRange: { 113 | wholeComp: window.settings.timeRange.value.wholeComp, 114 | workArea: window.settings.timeRange.value.workArea, 115 | layerDuration: window.settings.timeRange.value.layerDuration, 116 | // automatic: window.settings.timeRange.value.automatic 117 | }, 118 | selectedLayersOnly: window.settings.selectedLayersOnly, 119 | bakeTransforms: window.settings.bakeTransforms, 120 | frameSuperSampling: window.settings.frameSuperSampling, 121 | plugButton: window.buttons.plug.link, 122 | exportButton: window.buttons.doExport, 123 | cancelButton: window.buttons.cancel 124 | }; 125 | 126 | window.onShow = function() { 127 | // Give uniform width to all labels 128 | var groups = toArray(window.settings.children); 129 | var labelWidths = groups.map(function(group) { return group.children[0].size.width; }); 130 | var maxLabelWidth = Math.max.apply(Math, labelWidths); 131 | groups.forEach(function (group) { 132 | group.children[0].size.width = maxLabelWidth; 133 | }); 134 | 135 | // Give uniform width to inputs 136 | var valueWidths = groups.map(function(group) { 137 | return last(group.children).bounds.right - group.children[1].bounds.left; 138 | }); 139 | var maxValueWidth = Math.max.apply(Math, valueWidths); 140 | groups.forEach(function (group) { 141 | var multipleControls = group.children.length > 2; 142 | if (!multipleControls) { 143 | group.children[1].size.width = maxValueWidth; 144 | } 145 | }); 146 | 147 | window.layout.layout(true); 148 | }; 149 | 150 | /*window.onResize = function() { 151 | window.layout.resize(); 152 | window.layout.layout(); 153 | }*/ 154 | 155 | function getSettings() { 156 | var timeRange = null; 157 | for (var opt in controls.timeRange) { 158 | if (controls.timeRange.hasOwnProperty(opt) && 159 | controls.timeRange[opt].value) { 160 | timeRange = opt; 161 | break; 162 | } 163 | } 164 | var frameSuperSampling = parseInt(controls.frameSuperSampling.value.text); 165 | if (isNaN(frameSuperSampling) || frameSuperSampling < 1 || frameSuperSampling > 128) { 166 | frameSuperSampling = 1; 167 | } 168 | return { 169 | savePath: controls.savePath.text, 170 | timeRange: timeRange, 171 | selectedLayersOnly: controls.selectedLayersOnly.value.value, 172 | frameSuperSampling: frameSuperSampling, 173 | bakeTransforms: !!controls.bakeTransforms.value.value 174 | }; 175 | } 176 | 177 | /** 178 | * Apply previously-saved settings to the newly-created dialog 179 | */ 180 | function applySettings(settings) { 181 | if (!settings) return; 182 | controls.savePath.text = settings.savePath; 183 | for (var button in controls.timeRange) { 184 | if (!controls.timeRange.hasOwnProperty(button)) continue; 185 | controls.timeRange[button].value = button === settings.timeRange; 186 | } 187 | controls.bakeTransforms.value.value = settings.bakeTransforms; 188 | if (typeof settings.frameSuperSampling === 'number') { 189 | controls.frameSuperSampling.value.text = settings.frameSuperSampling; 190 | } 191 | } 192 | 193 | controls.plugButton.onClick = function() { 194 | var url = 'https://github.com/adroitwhiz/after-effects-to-blender-export'; 195 | system.callSystem(($.os.indexOf('Win') !== -1 ? 'explorer' : 'open') + ' "' + url + '"'); 196 | } 197 | 198 | controls.saveBrowse.onClick = function() { 199 | var savePath; 200 | // Use existing file path if set 201 | if (controls.savePath.text !== '') { 202 | savePath = new File(controls.savePath.text).saveDlg('Choose the path and name for the layer data file', 'Layer data:*.json').fsName; 203 | } else { 204 | savePath = File.saveDialog('Choose the path and name for the layer data file', 'Layer data:*.json').fsName; 205 | } 206 | // MacOS doesn't support a filter in the save dialog, so manually override the extension to .json 207 | if (!/\.json$/.test(savePath)) { 208 | savePath = savePath.replace(/(\.\w+)?$/, '.json'); 209 | } 210 | controls.savePath.text = savePath; 211 | } 212 | 213 | controls.exportButton.onClick = function() { 214 | try { 215 | cb(getSettings()); 216 | writeSettingsFile(getSettings(), settingsVersion); 217 | } catch (err) { 218 | showBugReportWindow(err); 219 | } 220 | window.close(); 221 | }; 222 | 223 | applySettings(readSettingsFile(settingsVersion)); 224 | 225 | return window; 226 | } 227 | 228 | function showBugReportWindow(err) { 229 | var c = controlFunctions; 230 | var resourceString = createResourceString( 231 | c.Dialog({ 232 | text: 'Error', 233 | header: c.StaticText({ 234 | text: 'The script has encountered an error. You can report it and paste the error message below into the report:', 235 | alignment: ['left', 'top'] 236 | }), 237 | errorInfo: c.EditText({ 238 | alignment: ['fill', 'fill'], 239 | minimumSize: [400, 100], 240 | properties: {multiline: true} 241 | }), 242 | separator: c.Group({ preferredSize: ['', 3] }), 243 | buttons: c.Group({ 244 | close: c.Button({ 245 | properties: { name: 'close' }, 246 | text: 'Close' 247 | }), 248 | alignment: 'right', 249 | report: c.Button({ 250 | properties: { name: 'report' }, 251 | text: 'Report Bug', 252 | active: true 253 | }) 254 | }) 255 | }) 256 | ); 257 | 258 | var window = new Window(resourceString, 'Error', undefined, {resizeable: false}); 259 | window.errorInfo.text = err.message; 260 | 261 | window.buttons.report.onClick = function() { 262 | var url = 'https://github.com/adroitwhiz/after-effects-to-blender-export/issues/new?assignees=&labels=bug%2C+export&template=issue-exporting-from-after-effects.md'; 263 | system.callSystem(($.os.indexOf('Win') !== -1 ? 'explorer' : 'open') + ' "' + url + '"'); 264 | } 265 | 266 | window.buttons.close.onClick = function() { 267 | window.close(); 268 | }; 269 | 270 | window.show(); 271 | } 272 | 273 | function checkPermissions() { 274 | if (app.preferences.getPrefAsLong('Main Pref Section', 'Pref_SCRIPTING_FILE_NETWORK_SECURITY') === 1) { 275 | return true; 276 | } 277 | var c = controlFunctions; 278 | var resourceString = createResourceString( 279 | c.Dialog({ 280 | text: 'Error', 281 | header: c.StaticText({ 282 | text: 'The "Allow Scripts to Write Files and Access Network" setting is disabled, and must be enabled in order for this script to work.', 283 | alignment: ['left', 'top'] 284 | }), 285 | separator: c.Group({ preferredSize: ['', 3] }), 286 | buttons: c.Group({ 287 | alignment: 'right', 288 | close: c.Button({ 289 | properties: { name: 'close' }, 290 | text: 'Close' 291 | }), 292 | openSettings: c.Button({ 293 | properties: { name: 'openSettings' }, 294 | text: 'Open Script Settings', 295 | active: true 296 | }) 297 | }) 298 | }) 299 | ); 300 | 301 | var window = new Window(resourceString, 'Error', undefined, {resizeable: false}); 302 | 303 | window.buttons.openSettings.onClick = function() { 304 | // Open the Scripting and Expressions settings dialog (command #3131, as documented... nowhere) 305 | // This won't work if called inside the callback for some reason (thanks Adobe!) so use scheduleTask 306 | app.scheduleTask("app.executeCommand(3131)", 0, false); 307 | window.close(); 308 | } 309 | 310 | window.buttons.close.onClick = function() { 311 | window.close(); 312 | } 313 | 314 | window.show(); 315 | return false; 316 | } 317 | 318 | function getCompositionViewer() { 319 | var project = app.project; 320 | if (!project) { 321 | throw new Error("Project does not exist."); 322 | } 323 | 324 | var activeViewer = app.activeViewer; 325 | if (activeViewer.type !== ViewerType.VIEWER_COMPOSITION) { 326 | throw new Error('Switch to the composition viewer.'); 327 | } 328 | 329 | return activeViewer; 330 | } 331 | 332 | function main() { 333 | if (!checkPermissions()) return; 334 | getCompositionViewer().setActive(); 335 | var activeComp = app.project.activeItem; 336 | if (!activeComp) { 337 | // This is the case if e.g. you've just created the project and the two big "New Composition" and 338 | // "New Composition From Footage" are showing where the composition viewer would be. The active viewer 339 | // is of ViewerType.VIEWER_COMPOSITION, but there's no actual composition open. 340 | throw new Error('No composition is currently open.'); 341 | } 342 | 343 | var d = showDialog( 344 | function(settings) { 345 | runExport(settings, { 346 | activeComp: activeComp 347 | }) 348 | }, 349 | { 350 | selectionExists: activeComp.selectedLayers.length > 0, 351 | frameSuperSampling: 1 352 | } 353 | ); 354 | d.window.show(); 355 | } 356 | 357 | function runExport(settings, opts) { 358 | var activeComp = opts.activeComp; 359 | var layersToExport = []; 360 | if (settings.selectedLayersOnly) { 361 | var layerIndicesMarkedForExport = {}; 362 | for (var i = 0; i < activeComp.selectedLayers.length; i++) { 363 | var layer = activeComp.selectedLayers[i]; 364 | layersToExport.push(layer); 365 | layerIndicesMarkedForExport[layer.index] = true; 366 | 367 | // If not baking transforms (also baking parents' transforms into child layers), 368 | // make sure to export all the selected layers' parents as well so that the children 369 | // can have the parent transforms applied to them 370 | if (!settings.bakeTransforms) { 371 | var parent = layer.parent; 372 | while (parent) { 373 | if (!(parent.index in layerIndicesMarkedForExport)) { 374 | layersToExport.push(parent); 375 | layerIndicesMarkedForExport[parent.index] = true; 376 | } 377 | parent = parent.parent; 378 | } 379 | } 380 | } 381 | } else { 382 | for (var i = 1; i <= activeComp.layers.length; i++) { 383 | var layer = activeComp.layers[i]; 384 | layersToExport.push(layer); 385 | } 386 | } 387 | 388 | var json = { 389 | layers: [], 390 | sources: [], 391 | comp: { 392 | width: activeComp.width, 393 | height: activeComp.height, 394 | name: activeComp.name, 395 | pixelAspect: activeComp.pixelAspect, 396 | frameRate: activeComp.frameRate, 397 | workArea: [activeComp.workAreaStart, activeComp.workAreaDuration + activeComp.workAreaStart] 398 | }, 399 | transformsBaked: settings.bakeTransforms, 400 | version: fileVersion 401 | }; 402 | 403 | var exportedSources = []; 404 | 405 | function unenum(val) { 406 | switch (val) { 407 | case KeyframeInterpolationType.LINEAR: return 'linear'; 408 | case KeyframeInterpolationType.BEZIER: return 'bezier'; 409 | case KeyframeInterpolationType.HOLD: return 'hold'; 410 | } 411 | 412 | throw new Error('Could not un-enum ' + val); 413 | } 414 | 415 | function startAndEndFrame(layer) { 416 | var startTime, duration; 417 | switch (settings.timeRange) { 418 | case 'workArea': 419 | startTime = activeComp.workAreaStart; 420 | duration = activeComp.workAreaDuration; 421 | break; 422 | case 'layerDuration': 423 | startTime = layer.inPoint; 424 | duration = layer.outPoint - layer.inPoint; 425 | break; 426 | case 'wholeComp': 427 | default: 428 | startTime = 0; 429 | duration = activeComp.duration; 430 | break; 431 | } 432 | // avoid floating point weirdness by rounding, just in case 433 | var startFrame = Math.floor(startTime * activeComp.frameRate); 434 | var endFrame = startFrame + Math.ceil(duration * activeComp.frameRate); 435 | return [startFrame, endFrame]; 436 | } 437 | 438 | function escapeStringForLiteral(str) { 439 | return str.replace(/(\\|")/g, '\\$1'); 440 | } 441 | 442 | if (settings.bakeTransforms) { 443 | // Adding a layer deselects all others, so save the original selection here. 444 | var selectedLayers = []; 445 | for (var i = 0; i < activeComp.selectedLayers.length; i++) { 446 | selectedLayers.push(activeComp.selectedLayers[i]); 447 | } 448 | 449 | // `toWorld` only works inside expressions, so add a null object whose expression we will set and then evaluate 450 | // using `valueAtTime`. 451 | var evaluator = activeComp.layers.addNull(); 452 | // Move the evaluator layer to the bottom to avoid messing up expressions which rely on layer indices 453 | evaluator.moveToEnd(); 454 | // Adding a new effect invalidates references to all other effects in the stack, so create all effects first 455 | // before obtaining references to them. I thought JS was a garbage-collected language, Adobe! 456 | for (var i = 0; i < 4; i++) { 457 | evaluator.property("Effects").addProperty("ADBE Point3D Control"); 458 | } 459 | var evalPoint1 = evaluator.property("Effects").property(1); 460 | var evalPoint2 = evaluator.property("Effects").property(2); 461 | var evalPoint3 = evaluator.property("Effects").property(3); 462 | var evalPoint4 = evaluator.property("Effects").property(4); 463 | } 464 | 465 | // Takes as input a list of 4 transformed points and returns the 3x4 affine transformation matrix that applies 466 | // such a transform. Assumes that the "source" points are [0, 0, 0], [1, 0, 0], [0, 1, 0], and [0, 0, 1]. 467 | // The 4th row will always be [0, 0, 0, 1], and is thus omitted. 468 | function pointsToAffineMatrix(p0, p1, p2, p3) { 469 | return [ 470 | p1[0] - p0[0], 471 | p2[0] - p0[0], 472 | p3[0] - p0[0], 473 | p0[0], 474 | p1[1] - p0[1], 475 | p2[1] - p0[1], 476 | p3[1] - p0[1], 477 | p0[1], 478 | p1[2] - p0[2], 479 | p2[2] - p0[2], 480 | p3[2] - p0[2], 481 | p0[2] 482 | ]; 483 | }; 484 | 485 | function exportBakedTransform(layer) { 486 | // It's possible to construct a 3D affine transform matrix given a mapping from 4 source points to 4 destination points. 487 | // The source points are the arguments of the toWorld functions, and the destination points are their results. 488 | // We calculate this affine transform matrix once per frame then decompose it on the Blender side. 489 | evalPoint1.property(1).expression = "thisComp.layer(\"" + escapeStringForLiteral(layer.name) + "\").toWorld([0, 0, 0])"; 490 | evalPoint2.property(1).expression = "thisComp.layer(\"" + escapeStringForLiteral(layer.name) + "\").toWorld([1, 0, 0])"; 491 | evalPoint3.property(1).expression = "thisComp.layer(\"" + escapeStringForLiteral(layer.name) + "\").toWorld([0, 1, 0])"; 492 | evalPoint4.property(1).expression = "thisComp.layer(\"" + escapeStringForLiteral(layer.name) + "\").toWorld([0, 0, 1])"; 493 | 494 | // Bake keyframe data 495 | var startEnd = startAndEndFrame(layer); 496 | var startFrame = startEnd[0]; 497 | var endFrame = startEnd[1]; 498 | 499 | var keyframes = []; 500 | 501 | for (var i = 0, n = (endFrame - startFrame) * settings.frameSuperSampling; i < n; i++) { 502 | var frame = (i / settings.frameSuperSampling) + startFrame; 503 | var time = frame / activeComp.frameRate; 504 | var point1Val = evalPoint1.property(1).valueAtTime(time, false /* preExpression */); 505 | var point2Val = evalPoint2.property(1).valueAtTime(time, false /* preExpression */); 506 | var point3Val = evalPoint3.property(1).valueAtTime(time, false /* preExpression */); 507 | var point4Val = evalPoint4.property(1).valueAtTime(time, false /* preExpression */); 508 | var matrix = pointsToAffineMatrix( 509 | point1Val, 510 | point2Val, 511 | point3Val, 512 | point4Val 513 | ); 514 | keyframes.push(matrix); 515 | } 516 | 517 | return { 518 | startFrame: startFrame, 519 | keyframes: keyframes, 520 | supersampling: settings.frameSuperSampling 521 | } 522 | } 523 | 524 | function exportProperty(prop, layer, exportedProp, channelOffset) { 525 | var valType = prop.propertyValueType; 526 | // The ternary conditional operator is left-associative in ExtendScript. HATE. HATE. HATE. HATE. HATE. HATE. HATE. HATE. 527 | var numDimensions = (valType === PropertyValueType.ThreeD || valType === PropertyValueType.ThreeD_SPATIAL ? 3 : 528 | (valType === PropertyValueType.TwoD || valType === PropertyValueType.TwoD_SPATIAL ? 2 : 529 | 1)); 530 | 531 | if (prop.isSeparationFollower && numDimensions > 1) { 532 | throw new Error('Separation follower cannot have more than 1 dimension'); 533 | } 534 | 535 | if (typeof exportedProp === 'undefined') { 536 | exportedProp = {numDimensions: numDimensions, channels: []}; 537 | for (var i = 0; i < numDimensions; i++) { 538 | exportedProp.channels.push({}); 539 | } 540 | } 541 | 542 | if (prop.isSeparationLeader && prop.dimensionsSeparated) { 543 | for (var i = 0; i < numDimensions; i++) { 544 | var dimProp = prop.getSeparationFollower(i); 545 | exportProperty(dimProp, layer, exportedProp, i); 546 | } 547 | 548 | return exportedProp; 549 | } 550 | 551 | if (typeof channelOffset === 'undefined') channelOffset = 0; 552 | 553 | if (prop.isTimeVarying) { 554 | if ( 555 | (!prop.expressionEnabled) && 556 | (valType === PropertyValueType.ThreeD || 557 | valType === PropertyValueType.TwoD || 558 | valType === PropertyValueType.OneD) 559 | ) { 560 | // Export keyframe Bezier data directly 561 | for (var i = 0; i < numDimensions; i++) { 562 | exportedProp.channels[i + channelOffset].isKeyframed = true; 563 | exportedProp.channels[i + channelOffset].keyframesFormat = 'bezier'; 564 | exportedProp.channels[i + channelOffset].keyframes = []; 565 | } 566 | 567 | for (var keyIndex = 1; keyIndex <= prop.numKeys; keyIndex++) { 568 | var time = prop.keyTime(keyIndex); 569 | var value = prop.keyValue(keyIndex); 570 | var easeIn = prop.keyInTemporalEase(keyIndex); 571 | var easeOut = prop.keyOutTemporalEase(keyIndex); 572 | var interpolationIn = unenum(prop.keyInInterpolationType(keyIndex)); 573 | var interpolationOut = unenum(prop.keyOutInterpolationType(keyIndex)); 574 | for (var i = 0; i < numDimensions; i++) { 575 | exportedProp.channels[i + channelOffset].keyframes.push({ 576 | value: Array.isArray(value) ? value[i] : value, 577 | easeIn: { 578 | speed: easeIn[i].speed, 579 | influence: easeIn[i].influence 580 | }, 581 | easeOut: { 582 | speed: easeOut[i].speed, 583 | influence: easeOut[i].influence 584 | }, 585 | time: prop.keyTime(keyIndex), 586 | interpolationIn: interpolationIn, 587 | interpolationOut: interpolationOut 588 | }) 589 | } 590 | } 591 | } else { 592 | // Bake keyframe data 593 | var startEnd = startAndEndFrame(layer); 594 | var startFrame = startEnd[0]; 595 | var endFrame = startEnd[1]; 596 | 597 | for (var i = 0; i < numDimensions; i++) { 598 | exportedProp.channels[i + channelOffset].isKeyframed = true; 599 | exportedProp.channels[i + channelOffset].keyframesFormat = 'calculated'; 600 | exportedProp.channels[i + channelOffset].startFrame = startFrame; 601 | exportedProp.channels[i + channelOffset].keyframes = []; 602 | exportedProp.channels[i + channelOffset].supersampling = settings.frameSuperSampling; 603 | } 604 | 605 | for (var i = 0, n = (endFrame - startFrame) * settings.frameSuperSampling; i < n; i++) { 606 | var frame = (i / settings.frameSuperSampling) + startFrame; 607 | var time = frame / activeComp.frameRate; 608 | var propVal = prop.valueAtTime(time, false /* preExpression */); 609 | for (var j = 0; j < numDimensions; j++) { 610 | exportedProp.channels[j + channelOffset].keyframes.push(Array.isArray(propVal) ? propVal[j] : propVal); 611 | } 612 | } 613 | } 614 | } else { 615 | for (var i = 0; i < numDimensions; i++) { 616 | exportedProp.channels[i + channelOffset].isKeyframed = false; 617 | exportedProp.channels[i + channelOffset].value = Array.isArray(prop.value) ? prop.value[i] : prop.value; 618 | } 619 | } 620 | 621 | return exportedProp; 622 | } 623 | 624 | function exportSource(source) { 625 | var exportedSource = { 626 | height: source.height, 627 | width: source.width, 628 | name: source.name 629 | }; 630 | if (source instanceof FootageItem) { 631 | if (source.mainSource instanceof SolidSource) { 632 | exportedSource.type = 'solid'; 633 | exportedSource.color = source.mainSource.color; 634 | } else if (source.mainSource instanceof FileSource) { 635 | exportedSource.type = 'file'; 636 | exportedSource.file = source.mainSource.file.absoluteURI; 637 | } else { 638 | exportedSource.type = 'unknown'; 639 | } 640 | } else { 641 | exportedSource.type = 'unknown'; 642 | } 643 | return exportedSource; 644 | } 645 | 646 | function exportLayer (layer) { 647 | var layerType; 648 | if (layer instanceof CameraLayer) { 649 | layerType = 'camera'; 650 | } else if (layer instanceof AVLayer) { 651 | layerType = 'av'; 652 | } else { 653 | layerType = 'unknown'; 654 | } 655 | 656 | var exportedObject = { 657 | name: layer.name, 658 | type: layerType, 659 | index: layer.index, 660 | parentIndex: layer.parent ? layer.parent.index : null, 661 | inFrame: layer.inPoint * activeComp.frameRate, 662 | outFrame: layer.outPoint * activeComp.frameRate, 663 | enabled: layer.enabled 664 | }; 665 | 666 | if (settings.bakeTransforms) { 667 | exportedObject.transform = exportBakedTransform(layer); 668 | } else { 669 | exportedObject.position = exportProperty(layer.position, layer); 670 | exportedObject.rotationX = exportProperty(layer.xRotation, layer); 671 | exportedObject.rotationY = exportProperty(layer.yRotation, layer); 672 | exportedObject.rotationZ = exportProperty(layer.rotation, layer); 673 | exportedObject.orientation = exportProperty(layer.orientation, layer); 674 | } 675 | 676 | if (layer instanceof CameraLayer) { 677 | exportedObject.zoom = exportProperty(layer.zoom, layer); 678 | } 679 | 680 | // The "Point of Interest" property exists and is not hidden, meaning it's taking effect 681 | // Interestingly, `pointOfInterest.canSetExpression` is always true for other layer types 682 | if ( 683 | (layer instanceof CameraLayer || layer instanceof LightLayer) && 684 | layer.pointOfInterest.canSetExpression && 685 | !settings.bakeTransforms 686 | ) { 687 | exportedObject.pointOfInterest = exportProperty(layer.pointOfInterest, layer); 688 | } 689 | 690 | if (layer instanceof AVLayer) { 691 | // Export layer source 692 | var alreadyExported = false; 693 | for (var i = 0; i < exportedSources.length; i++) { 694 | if (exportedSources[i] === layer.source) { 695 | alreadyExported = true; 696 | break; 697 | } 698 | } 699 | if (!alreadyExported) { 700 | exportedSources.push(layer.source); 701 | json.sources.push(exportSource(layer.source)); 702 | } 703 | exportedObject.source = exportedSources.indexOf(layer.source); 704 | 705 | if (!settings.bakeTransforms) { 706 | exportedObject.anchorPoint = exportProperty(layer.anchorPoint, layer); 707 | exportedObject.scale = exportProperty(layer.scale, layer); 708 | } 709 | exportedObject.opacity = exportProperty(layer.opacity, layer); 710 | exportedObject.nullLayer = layer.nullLayer; 711 | } 712 | 713 | return exportedObject; 714 | } 715 | 716 | try { 717 | for (var j = 0; j < layersToExport.length; j++) { 718 | try { 719 | var exportedLayer = exportLayer(layersToExport[j]); 720 | json.layers.push(exportedLayer); 721 | } catch (err) { 722 | // Give specific information on what layer is causing the problem 723 | // This allows the user to fix it by deselecting the layer, and makes debugging easier 724 | throw new Error('Error exporting layer "' + layersToExport[j].name + '"\nOn line ' + err.line + ': ' + err.message); 725 | } 726 | } 727 | } finally { 728 | if (settings.bakeTransforms) { 729 | evaluator.remove(); 730 | for (var i = 0; i < selectedLayers.length; i++) { 731 | selectedLayers[i].selected = true; 732 | } 733 | } 734 | } 735 | 736 | var savePath = settings.savePath.replace(/\.\w+$/, '.json'); 737 | writeTextFile(savePath, JSON.stringify(json, null, 2)); 738 | } 739 | 740 | try { 741 | // If we can't get the composition viewer, fail early rather than baiting the user into filling out all the 742 | // settings in the dialog just for it to fail when they click Export 743 | getCompositionViewer(); 744 | main(); 745 | } catch (err) { 746 | alert(err.message, 'Error'); 747 | } 748 | } -------------------------------------------------------------------------------- /import-comp-to-blender/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | import bpy 3 | from bpy.types import Action, FCurve, Camera, TimelineMarker, Object 4 | from typing import Callable, Optional, Tuple, Protocol 5 | from bpy_extras.io_utils import ImportHelper 6 | from mathutils import Euler, Matrix, Quaternion, Vector 7 | from math import radians, pi, floor, ceil, isclose 8 | from fractions import Fraction 9 | from itertools import chain 10 | from dataclasses import dataclass 11 | from abc import abstractmethod 12 | 13 | @dataclass 14 | class CameraLayer: 15 | """Camera layer; used for Cameras to Markers functionality""" 16 | camera: Object 17 | inFrame: int 18 | outFrame: int 19 | 20 | class IActionSlotManager(Protocol): 21 | @abstractmethod 22 | def fcurve_for_data_path(self, dst_obj: 'bpy.types.Object', ae_obj: dict, data_path: str, index = -1) -> 'FCurve': 23 | raise NotImplementedError 24 | 25 | ''' 26 | Creates and maps imported AE objects to animation action slots. Some AE objects may be imported as nested sets of 27 | Blender objects, in which case both of them should get different slots in the same action. 28 | 29 | Once Blender supports layering actions, we will likely be able to avoid encoding orientation transforms as a set of two 30 | nested objects and just stack multiple action layers instead. 31 | ''' 32 | class ActionSlotManager: 33 | actions_by_ae_object: dict[str, Action] 34 | slots_by_bpy_object: dict[str, 'bpy.types.ActionSlot'] 35 | 36 | def __init__(self): 37 | self.actions_by_ae_object = dict() 38 | self.slots_by_bpy_object = dict() 39 | 40 | def fcurve_for_data_path(self, dst_obj: 'bpy.types.Object', ae_obj: dict, data_path: str, index = -1) -> 'FCurve': 41 | ''' 42 | Returns an F-curve for a given data path on the specified Blender object, which corresponds to a certain After 43 | Effects layer. Creates one if it does not exist. 44 | 45 | Args: 46 | dst_obj: The Blender object to get or create the F-curve on. 47 | 48 | ae_obj: The After Effects layer that this Blender object corresponds to. Note that more than one Blender 49 | object may correspond to the same After Effects layer, in which case it will be assigned to a different 50 | action slot. 51 | 52 | data_path: The Blender object's datapath which the F-curve controls. 53 | 54 | index (int, optional): The index of the property, for multidimensional properties like location, rotation, 55 | and scale. 56 | ''' 57 | 58 | ae_name = ae_obj['name'] 59 | 60 | # Create the action for this AE layer if it does not exist 61 | try: 62 | action = self.actions_by_ae_object[ae_name] 63 | layer = action.layers.values()[0] 64 | strip = layer.strips.values()[0] 65 | except KeyError: 66 | action = bpy.data.actions.new(f'AE {ae_name} Action') 67 | layer = action.layers.new('Layer') 68 | strip = layer.strips.new(type='KEYFRAME') 69 | self.actions_by_ae_object[ae_name] = action 70 | 71 | # Get the slot within the action for this specific Blender object 72 | try: 73 | slot = self.slots_by_bpy_object[dst_obj.name] 74 | except KeyError: 75 | slot = action.slots.new(id_type=dst_obj.id_type, name=dst_obj.name) 76 | if dst_obj.animation_data is None: 77 | dst_obj.animation_data_create() 78 | dst_obj.animation_data.action = action 79 | dst_obj.animation_data.action_slot = slot 80 | 81 | self.slots_by_bpy_object[dst_obj.name] = slot 82 | 83 | channelbag = strip.channelbag(slot, ensure=True) 84 | 85 | fc = channelbag.fcurves.find(data_path, index=index) 86 | if fc is None: 87 | fc = channelbag.fcurves.new(data_path, index=index) 88 | 89 | return fc 90 | 91 | ''' 92 | Class that's compatible with ActionSlotManager but made for pre-4.4 versions of Blender. 93 | ''' 94 | class LegacyActionSlotManager: 95 | actions_by_ae_object: dict[str, Action] 96 | def __init__(self): 97 | self.actions_by_ae_object = dict() 98 | 99 | def fcurve_for_data_path(self, dst_obj: 'bpy.types.Object', ae_obj: dict, data_path: str, index = -1) -> 'FCurve': 100 | ''' 101 | Returns an F-curve for a given data path on the specified Blender object, which corresponds to a certain After 102 | Effects layer. Creates one if it does not exist. 103 | 104 | Args: 105 | dst_obj: The Blender object to get or create the F-curve on. 106 | 107 | ae_obj: The After Effects layer that this Blender object corresponds to. This is ignored for the 108 | LegacyActionSlotManager. 109 | 110 | data_path: The Blender object's datapath which the F-curve controls. 111 | 112 | index (int, optional): The index of the property, for multidimensional properties like location, rotation, 113 | and scale. 114 | ''' 115 | 116 | if dst_obj.animation_data is None: 117 | dst_obj.animation_data_create() 118 | if dst_obj.animation_data.action is None: 119 | dst_obj.animation_data.action = bpy.data.actions.new(dst_obj.name + 'Action') 120 | 121 | action = dst_obj.animation_data.action 122 | for fc in action.fcurves: 123 | if (fc.data_path != data_path): 124 | continue 125 | if index<0 or index==fc.array_index: 126 | return fc 127 | 128 | # the action didn't have the fcurve we needed, yet 129 | return action.fcurves.new(data_path, index=index) 130 | 131 | class ImportAEComp(bpy.types.Operator, ImportHelper): 132 | """Import layers from an After Effects composition, as exported by the corresponding AE script""" 133 | bl_idname = "import.ae_comp" 134 | bl_label = "Import AE Comp" 135 | bl_options = {"REGISTER", "UNDO", "PRESET"} 136 | filename_ext = ".json" 137 | filter_glob: bpy.props.StringProperty( 138 | default="*.json", 139 | options={'HIDDEN'}, 140 | ) 141 | 142 | scale_factor: bpy.props.FloatProperty( 143 | name="Scale Factor", 144 | description="Amount to scale the imported layers by. The default (0.01) maps one pixel to one centimeter", 145 | min=0.0001, 146 | max=10000.0, 147 | default=0.01 148 | ) 149 | 150 | handle_framerate: bpy.props.EnumProperty( 151 | items=[ 152 | ( 153 | "preserve_frame_numbers", 154 | "Preserve Frame Numbers", 155 | "Keep the frame numbers the same, without changing the Blender scene's frame rate, if frame rates differ", 156 | "", 157 | 0 158 | ), ( 159 | "set_framerate", 160 | "Use Comp Frame Rate", 161 | "Set the Blender scene's frame rate to match that of the imported composition", 162 | "", 163 | 1 164 | ), ( 165 | "remap_times", 166 | "Remap Frame Times", 167 | "If the Blender scene's frame rate differs, preserve the speed of the imported composition by changing frame numbers", 168 | "", 169 | 2 170 | ), 171 | ], 172 | name="Handle FPS", 173 | description="How to handle the frame rate of the imported composition differing from the Blender scene's frame rate", 174 | default="preserve_frame_numbers" 175 | ) 176 | 177 | comp_center_to_origin: bpy.props.BoolProperty( 178 | name="Comp Center to Origin", 179 | description="Translate everything over so that the composition center is at (0, 0, 0) (the origin)", 180 | default=False 181 | ) 182 | 183 | use_comp_resolution: bpy.props.BoolProperty( 184 | name="Use Comp Resolution", 185 | description="Change the scene resolution and pixel aspect ratio to those of the imported composition", 186 | default=False 187 | ) 188 | 189 | create_new_collection: bpy.props.BoolProperty( 190 | name="Create New Collection", 191 | description="Add all the imported layers to a new collection.", 192 | default=False 193 | ) 194 | 195 | adjust_frame_start_end: bpy.props.BoolProperty( 196 | name="Adjust Frame Start/End", 197 | description="Adjust the Start and End frames of the playback/rendering range to the imported composition's work area.", 198 | default=False 199 | ) 200 | 201 | cameras_to_markers: bpy.props.BoolProperty( 202 | name="Cameras to Markers", 203 | description="Create timeline markers and bind them to the imported camera layers' in/out points.", 204 | default=False 205 | ) 206 | 207 | def import_bezier_keyframe_channel(self, fcurve: 'FCurve', keyframes, framerate: float, mul = 1.0, add = 0.0): 208 | '''Imports a given keyframe channel in Bezier format onto a given F-curve. 209 | 210 | Args: 211 | fcurve (FCurve): The F-curve to import the keyframes into. 212 | keyframes: The keyframes. 213 | framerate (float): The scene's framerate. 214 | mul (float, optional): Multiply all keyframes by this value. Defaults to 1. 215 | add (float, optional): Add this value to all keyframes. Defaults to 0. 216 | ''' 217 | fcurve.keyframe_points.add(len(keyframes)) 218 | for i, keyframe in enumerate(keyframes): 219 | k = fcurve.keyframe_points[i] 220 | if keyframe['interpolationOut'] == 'hold': 221 | k.interpolation = 'CONSTANT' 222 | k.handle_left_type = 'FREE' 223 | k.handle_right_type = 'FREE' 224 | x = keyframe['time'] * framerate 225 | y = keyframe['value'] 226 | k.co = [x, y * mul + add] 227 | if i > 0: 228 | # After Effects keyframe handles have a "speed" (in units per second) which determines the vertical 229 | # position of the handle, and an "influence" (as a percentage of the distance to the previous/next 230 | # keyframe) which determines the horizontal position and also scales the vertical position. 231 | easeIn = keyframe['easeIn'] 232 | prev_to_cur_duration = (keyframe['time'] - keyframes[i - 1]['time']) * framerate 233 | influence = easeIn['influence'] * 0.01 234 | k.handle_left = [ 235 | x - (prev_to_cur_duration * influence), 236 | (y - (easeIn['speed'] * influence * (prev_to_cur_duration / framerate))) * mul + add 237 | ] 238 | if i != len(keyframes) - 1: 239 | easeOut = keyframe['easeOut'] 240 | cur_to_next_duration = (keyframes[i + 1]['time'] - keyframe['time']) * framerate 241 | influence = easeOut['influence'] * 0.01 242 | k.handle_right = [ 243 | x + (cur_to_next_duration * influence), 244 | (y + (easeOut['speed'] * influence * (cur_to_next_duration / framerate))) * mul + add 245 | ] 246 | 247 | def import_baked_keyframe_channel( 248 | self, 249 | fcurve: FCurve, 250 | keyframes, 251 | start_frame: int, 252 | comp_framerate: float, 253 | desired_framerate: float, 254 | supersampling_rate: int, 255 | mul = 1.0, 256 | add = 0.0): 257 | '''Import a given keyframe channel in "calculated"/baked format onto a given F-curve. 258 | 259 | Args: 260 | fcurve (FCurve): The F-curve to import the keyframes into. 261 | keyframes: The keyframes. 262 | start_frame (int): The frame number at which the keyframe data starts. 263 | comp_framerate (float): The comp's framerate. 264 | desired_framerate (float): The desired framerate. 265 | supersampling_rate (int): Multiplier for the framerate; this many keyframes will be created per frame. 266 | mul (int, optional): Multiply all keyframes by this value. Defaults to 1. 267 | add (int, optional): Add this value to all keyframes. Defaults to 0. 268 | ''' 269 | fcurve.keyframe_points.add(len(keyframes)) 270 | for i, keyframe in enumerate(keyframes): 271 | k = fcurve.keyframe_points[i] 272 | k.co_ui = [(((i / supersampling_rate) + start_frame) * desired_framerate) / comp_framerate, keyframe * mul + add] 273 | k.interpolation = 'LINEAR' 274 | 275 | def import_property( 276 | self, 277 | slot_mgr: IActionSlotManager, 278 | obj: 'bpy.types.Object', 279 | ae_obj: dict, 280 | data_path: str, 281 | data_index: int, 282 | prop_data, 283 | comp_framerate: float, 284 | desired_framerate: float, 285 | mul = 1.0, 286 | add = 0.0): 287 | '''Imports a given property from the JSON file onto a given Blender object. 288 | 289 | Args: 290 | slot_mgr (IActionSlotManager): Object for managing animation action slots. 291 | obj (bpy_struct): The object to import the property onto. 292 | ae_obj (bpy_struct): The After Effects layer object that this property is part of. 293 | data_path (str): The destination data path of the property. 294 | data_index (int): The index into the destination data path, for multidimensional properties. -1 for single-dimension properties. 295 | prop_data: The JSON property data. 296 | comp_framerate (float): The comp's framerate. 297 | desired_framerate (float): The desired framerate. 298 | mul (float, optional): Multiply the property by this value. Defaults to 1. 299 | add (float, optional): Add this value to the property. Defaults to 0. 300 | ''' 301 | if prop_data['isKeyframed']: 302 | fcurve = slot_mgr.fcurve_for_data_path(obj, ae_obj, data_path, data_index) 303 | if prop_data['keyframesFormat'] == 'bezier': 304 | self.import_bezier_keyframe_channel( 305 | fcurve, 306 | prop_data['keyframes'], 307 | desired_framerate, 308 | mul, 309 | add 310 | ) 311 | else: 312 | self.import_baked_keyframe_channel( 313 | fcurve, 314 | prop_data['keyframes'], 315 | prop_data['startFrame'], 316 | comp_framerate, 317 | desired_framerate, 318 | prop_data.get('supersampling', 1), 319 | mul, 320 | add 321 | ) 322 | else: 323 | cur_val = getattr(obj, data_path) 324 | if data_index == -1: 325 | cur_val = prop_data['value'] * mul + add 326 | else: 327 | cur_val[data_index] = prop_data['value'] * mul + add 328 | setattr(obj, data_path, cur_val) 329 | 330 | def import_baked_transform( 331 | self, 332 | slot_mgr: IActionSlotManager, 333 | obj: 'bpy.types.Object', 334 | ae_obj: dict, 335 | data, 336 | comp_framerate: float, 337 | desired_framerate: float, 338 | func: Optional[ 339 | Callable[ 340 | ['Vector', 'Quaternion', 'Vector'], 341 | Tuple['Vector', 'Quaternion', 'Vector'] 342 | ] 343 | ] = None): 344 | '''Import a baked transform (one 4x4 transform matrix per frame) onto a given Blender object. 345 | 346 | Args: 347 | slot_mgr (IActionSlotManager): Object for managing animation action slots. 348 | obj (bpy_struct): The object to import the property onto. 349 | ae_obj (bpy_struct): The After Effects layer object that this property is part of. 350 | data: The JSON transform data. 351 | comp_framerate (float): The comp's framerate. 352 | desired_framerate (float): The desired framerate. 353 | func ((Vector, Quaternion, Vector) -> (Vector, Quaternion, Vector), optional): Function to call on each keyframe. 354 | ''' 355 | obj.rotation_mode = 'QUATERNION' 356 | 357 | loc_fcurves = [slot_mgr.fcurve_for_data_path(obj, ae_obj, 'location', index) for index in range(3)] 358 | rot_fcurves = [slot_mgr.fcurve_for_data_path(obj, ae_obj, 'rotation_quaternion', index) for index in range(4)] 359 | scale_fcurves = [slot_mgr.fcurve_for_data_path(obj, ae_obj, 'scale', index) for index in range(3)] 360 | 361 | keyframes = data['keyframes'] 362 | start_frame = data['startFrame'] 363 | supersampling_rate = data.get('supersampling', 1) 364 | 365 | for fcurve in chain(loc_fcurves, rot_fcurves, scale_fcurves): 366 | fcurve.keyframe_points.add(len(keyframes)) 367 | 368 | prev_rot = None 369 | for i, keyframe in enumerate(keyframes): 370 | mat = Matrix(( 371 | (keyframe[0], keyframe[1], keyframe[2], keyframe[3]), 372 | (keyframe[4], keyframe[5], keyframe[6], keyframe[7]), 373 | (keyframe[8], keyframe[9], keyframe[10], keyframe[11]), 374 | (0.0, 0.0, 0.0, 1.0) 375 | )) 376 | loc, rot, scale = mat.decompose() 377 | 378 | if func is not None: 379 | loc, rot, scale = func(loc, rot, scale) 380 | 381 | if prev_rot is not None: 382 | rot.make_compatible(prev_rot) 383 | prev_rot = rot 384 | 385 | kx = (((i / supersampling_rate) + start_frame) * desired_framerate) / comp_framerate 386 | for j in range(3): 387 | k = loc_fcurves[j].keyframe_points[i] 388 | k.co_ui = [kx, loc[j]] 389 | k.interpolation = 'LINEAR' 390 | for j in range(4): 391 | k = rot_fcurves[j].keyframe_points[i] 392 | k.co_ui = [kx, rot[j]] 393 | k.interpolation = 'LINEAR' 394 | for j in range(3): 395 | k = scale_fcurves[j].keyframe_points[i] 396 | k.co_ui = [kx, scale[j]] 397 | k.interpolation = 'LINEAR' 398 | 399 | def import_property_spatial( 400 | self, 401 | slot_mgr: IActionSlotManager, 402 | obj: 'bpy.types.Object', 403 | ae_obj: dict, 404 | data_path: str, 405 | prop_data, 406 | comp_framerate: float, 407 | desired_framerate: float, 408 | swizzle: Tuple[int, int, int], 409 | mul: Tuple[float, float, float], 410 | add: Tuple[float, float, float] = (0.0, 0.0, 0.0) 411 | ): 412 | '''Import a 3D spatial property. 413 | 414 | Args: 415 | slot_mgr (IActionSlotManager): Object for managing animation action slots. 416 | obj (bpy_struct): The object to import the property onto. 417 | ae_obj (bpy_struct): The After Effects layer object that this property is part of. 418 | data_path (str): The destination property's data path. 419 | prop_data: The JSON property data. 420 | comp_framerate (float): The comp's framerate. 421 | desired_framerate (float): The desired framerate. 422 | swizzle (int, int, int): The indices to place the destination values onto (e.g. (0, 2, 1) to map 423 | the channel with source index 1 to destination index 2, and vice versa). 424 | mul (float, float, float): The (pre-swizzle) values to multiply the keyframe values by. 425 | add (float, float, float): The (pre-swizzle) values to add to the keyframe values. 426 | ''' 427 | for i in range(3): 428 | self.import_property( 429 | slot_mgr=slot_mgr, 430 | obj=obj, 431 | ae_obj=ae_obj, 432 | data_path=data_path, 433 | data_index=swizzle[i], 434 | prop_data=prop_data['channels'][i], 435 | comp_framerate=comp_framerate, 436 | desired_framerate=desired_framerate, 437 | mul=mul[i], 438 | add=add[i] 439 | ) 440 | 441 | def execute(self, context): 442 | scale_factor = self.scale_factor 443 | 444 | with open(self.filepath) as f: 445 | data = json.load(f) 446 | 447 | fileVersion = data.get('version') 448 | if fileVersion != 3: 449 | if fileVersion is None: 450 | warning = 'This isn\'t a valid exported file in the correct format.' 451 | elif fileVersion > 3: 452 | warning = 'This file is too new. Update this add-on.' 453 | else: 454 | warning = 'This file is too old. Re-export it using a newer version of this add-on.' 455 | self.report({'WARNING'}, warning) 456 | return {'CANCELLED'} 457 | 458 | if hasattr(bpy.types, 'ActionSlot'): 459 | slot_mgr = ActionSlotManager() 460 | else: 461 | slot_mgr = LegacyActionSlotManager() 462 | added_objects = [] 463 | cameras: list[CameraLayer] = [] 464 | camera_in_out_frames: list[int] = [] 465 | 466 | imported_objects = [] 467 | innermost_objects_by_index = dict() 468 | 469 | 470 | if self.handle_framerate == 'remap_times': 471 | desired_framerate = context.scene.render.fps 472 | else: 473 | desired_framerate = data['comp']['frameRate'] 474 | 475 | if self.handle_framerate == 'set_framerate': 476 | comp_framerate = data['comp']['frameRate'] 477 | if int(comp_framerate) == comp_framerate: 478 | context.scene.render.fps = comp_framerate 479 | context.scene.render.fps_base = 1.0 480 | else: 481 | ceil_framerate = ceil(comp_framerate) 482 | # round to 1.001, the proper timebase 483 | fps_base = round(ceil_framerate / comp_framerate, 5) 484 | context.scene.render.fps = ceil_framerate 485 | context.scene.render.fps_base = fps_base 486 | 487 | for layer in data['layers']: 488 | if layer['type'] == 'av': 489 | if 'nullLayer' in layer and layer['nullLayer']: 490 | obj_data = None 491 | else: 492 | width = data['sources'][layer['source']]['width'] * scale_factor 493 | height = data['sources'][layer['source']]['height'] * scale_factor 494 | verts = [ 495 | (0, 0, -height), 496 | (width, 0, -height), 497 | (width, 0, 0), 498 | (0, 0, 0) 499 | ] 500 | obj_data = bpy.data.meshes.new(layer['name']) 501 | obj_data.from_pydata(verts, [], [[0, 1, 2, 3]]) 502 | obj_data.uv_layers.new() 503 | elif layer['type'] == 'camera': 504 | obj_data = bpy.data.cameras.new(layer['name']) 505 | elif layer['type'] == 'unknown': 506 | obj_data = None 507 | obj = bpy.data.objects.new(layer['name'], obj_data) 508 | if layer['type'] == 'camera' and 'enabled' in layer and layer['enabled'] and 'inFrame' in layer and 'outFrame' in layer: 509 | # If this is an enabled camera layer, add it to the "Camera to Markers" data to be imported 510 | # Older files don't have inFrame/outFrame/enabled properties, so confirm their presence 511 | # The frames are calculated by multiplying floating-point seconds values by the framerate, so they're 512 | # often a bit off and need to be rounded to the nearest frame 513 | cameras.append(CameraLayer(obj, round(layer['inFrame']), round(layer['outFrame']))) 514 | camera_in_out_frames.append(round(layer['inFrame'])) 515 | camera_in_out_frames.append(round(layer['outFrame'])) 516 | added_objects.append(obj) 517 | 518 | innermost_objects_by_index[layer['index']] = obj 519 | 520 | transform_target = obj 521 | 522 | if data['transformsBaked']: 523 | # These are used to swap Z and -Y. Not sure this is the best way to do it. 524 | pre_quat = Quaternion((1.0, 0.0, 0.0), radians(-90.0)) 525 | post_quat = Quaternion((1.0, 0.0, 0.0), radians(180.0 if layer['type'] == 'camera' else 90.0)) 526 | def transform(loc, rot, scale): 527 | if self.comp_center_to_origin: 528 | loc -= Vector((data['comp']['width'] * 0.5, data['comp']['height'] * 0.5, 0.0)) 529 | loc = Vector((loc[0] * scale_factor, loc[2] * scale_factor, -loc[1] * scale_factor)) 530 | scale = Vector((scale[0], scale[2], scale[1])) 531 | rot = pre_quat @ rot @ post_quat 532 | return loc, rot, scale 533 | 534 | self.import_baked_transform( 535 | slot_mgr, 536 | obj, 537 | layer, 538 | layer['transform'], 539 | comp_framerate=data['comp']['frameRate'], 540 | desired_framerate=desired_framerate, 541 | func=transform 542 | ) 543 | else: 544 | if 'anchorPoint' in layer and ( 545 | any(channel['isKeyframed'] for channel in layer['anchorPoint']['channels']) or 546 | any(abs(channel['value']) >= 1e-15 for channel in layer['anchorPoint']['channels']) 547 | ): 548 | anchor_parent = bpy.data.objects.new(layer['name'] + ' Anchor Point', None) 549 | anchor_parent.empty_display_type = 'ARROWS' 550 | added_objects.append(anchor_parent) 551 | self.import_property_spatial( 552 | slot_mgr=slot_mgr, 553 | obj=transform_target, 554 | ae_obj=layer, 555 | data_path='location', 556 | prop_data=layer['anchorPoint'], 557 | comp_framerate=data['comp']['frameRate'], 558 | desired_framerate=desired_framerate, 559 | swizzle=(0, 2, 1), 560 | mul=(-scale_factor, scale_factor, -scale_factor) 561 | ) 562 | transform_target.parent = anchor_parent 563 | transform_target = anchor_parent 564 | 565 | if 'scale' in layer: 566 | self.import_property_spatial( 567 | slot_mgr=slot_mgr, 568 | obj=transform_target, 569 | ae_obj=layer, 570 | data_path='scale', 571 | prop_data=layer['scale'], 572 | comp_framerate=data['comp']['frameRate'], 573 | desired_framerate=desired_framerate, 574 | swizzle=(0, 2, 1), 575 | mul=(0.01, 0.01, 0.01) 576 | ) 577 | 578 | ANGLE_CONVERSION_FACTOR = pi / 180 579 | if layer['type'] == 'camera': 580 | # Rotate camera upwards 90 degrees along the X axis 581 | transform_target.rotation_mode = 'ZYX' 582 | channel_swizzle = (0, 1, 2) 583 | channel_add = (pi / 2, 0, 0) 584 | channel_multiply = (1, -1, -1) 585 | else: 586 | transform_target.rotation_mode = 'YZX' 587 | channel_swizzle = (0, 2, 1) 588 | channel_add = (0, 0, 0) 589 | channel_multiply = (1, -1, 1) 590 | 591 | for index, prop_name in enumerate(['rotationX', 'rotationY', 'rotationZ']): 592 | if prop_name in layer: 593 | self.import_property( 594 | slot_mgr=slot_mgr, 595 | obj=transform_target, 596 | ae_obj=layer, 597 | data_path='rotation_euler', 598 | data_index=channel_swizzle[index], 599 | prop_data=layer[prop_name]['channels'][0], 600 | comp_framerate=data['comp']['frameRate'], 601 | desired_framerate=desired_framerate, 602 | mul=ANGLE_CONVERSION_FACTOR * channel_multiply[index], 603 | add=channel_add[index] 604 | ) 605 | 606 | if 'orientation' in layer: 607 | all_keyframed = all(channel['isKeyframed'] for channel in layer['orientation']['channels']) 608 | none_keyframed = all(not channel['isKeyframed'] for channel in layer['orientation']['channels']) 609 | 610 | if not (all_keyframed or none_keyframed): 611 | raise ValueError('Orientation keyframe channels should either all be keyframed or all be not keyframed') 612 | 613 | if not (none_keyframed and all(abs(channel['value']) < 1e-15 for channel in layer['orientation']['channels'])): 614 | orientation_parent = bpy.data.objects.new(layer['name'] + ' Orientation', None) 615 | orientation_parent.empty_display_type = 'ARROWS' 616 | added_objects.append(orientation_parent) 617 | 618 | if all_keyframed: 619 | if any(channel['keyframesFormat'] != 'calculated' for channel in layer['orientation']['channels']): 620 | raise ValueError('Orientation keyframes must be in "calculated" format') 621 | 622 | orientation_parent.rotation_mode = 'QUATERNION' 623 | num_keyframes = len(layer['orientation']['channels'][0]['keyframes']) 624 | start_frame = layer['orientation']['channels'][0]['startFrame'] 625 | rot_fcurves = [slot_mgr.fcurve_for_data_path(orientation_parent, layer, 'rotation_quaternion', i) for i in range(4)] 626 | for fcurve in rot_fcurves: 627 | fcurve.keyframe_points.add(num_keyframes) 628 | for i in range(num_keyframes): 629 | fcurve.keyframe_points[i].interpolation = 'LINEAR' 630 | 631 | prev_angle = None 632 | for i, (x, y, z) in enumerate(zip(*(channel['keyframes'] for channel in layer['orientation']['channels']))): 633 | angle = Matrix.Identity(3) 634 | # Apply AE orientation 635 | angle.rotate(Euler((radians(x), radians(z), radians(-y)), 'YZX')) 636 | # Prevent discontinuities in the rotation which can mess up motion blur. 637 | # Euler angles also have a make_compatible function, but it doesn't always work, so it's necessary to use quaternions. 638 | quat = angle.to_quaternion() 639 | if prev_angle: 640 | quat.make_compatible(prev_angle) 641 | 642 | prev_angle = quat 643 | 644 | for j in range(4): 645 | k = rot_fcurves[j].keyframe_points[i] 646 | k.co_ui = [i + start_frame, quat[j]] 647 | else: 648 | orientation_parent.rotation_mode = 'YZX' 649 | orientation_parent.rotation_euler = [ 650 | radians(layer['orientation']['channels'][0]['value']), 651 | radians(layer['orientation']['channels'][2]['value']), 652 | radians(-layer['orientation']['channels'][1]['value']) 653 | ] 654 | 655 | transform_target.parent = orientation_parent 656 | transform_target = orientation_parent 657 | 658 | if 'pointOfInterest' in layer: 659 | point_of_interest_parent = bpy.data.objects.new(layer['name'] + ' Point Of Interest Constraint', None) 660 | point_of_interest_parent.empty_display_type = 'ARROWS' 661 | added_objects.append(point_of_interest_parent) 662 | 663 | point_of_interest = bpy.data.objects.new(layer['name'] + ' Point Of Interest', None) 664 | added_objects.append(point_of_interest) 665 | 666 | self.import_property_spatial( 667 | slot_mgr=slot_mgr, 668 | obj=point_of_interest, 669 | ae_obj=layer, 670 | data_path='location', 671 | prop_data=layer['pointOfInterest'], 672 | comp_framerate=data['comp']['frameRate'], 673 | desired_framerate=desired_framerate, 674 | swizzle=(0, 2, 1), 675 | mul=(scale_factor, -scale_factor, scale_factor), 676 | add=( 677 | # TODO: abstract the process of "comp center to origin" for all translations 678 | -data['comp']['width'] * 0.5 * self.scale_factor if self.comp_center_to_origin else 0, 679 | data['comp']['height'] * 0.5 * self.scale_factor if self.comp_center_to_origin else 0, 680 | 0 681 | ) 682 | ) 683 | 684 | track_constraint = point_of_interest_parent.constraints.new('TRACK_TO') 685 | track_constraint.owner_space = 'LOCAL' 686 | track_constraint.target = point_of_interest 687 | track_constraint.track_axis = 'TRACK_Y' 688 | track_constraint.up_axis = 'UP_Z' 689 | 690 | transform_target.parent = point_of_interest_parent 691 | transform_target = point_of_interest_parent 692 | 693 | should_translate = self.comp_center_to_origin and layer['parentIndex'] is None 694 | 695 | if 'position' in layer: 696 | self.import_property_spatial( 697 | slot_mgr=slot_mgr, 698 | obj=transform_target, 699 | ae_obj=layer, 700 | data_path='location', 701 | prop_data=layer['position'], 702 | comp_framerate=data['comp']['frameRate'], 703 | desired_framerate=desired_framerate, 704 | swizzle=(0, 2, 1), 705 | mul=(scale_factor, -scale_factor, scale_factor), 706 | add=( 707 | -data['comp']['width'] * 0.5 * self.scale_factor if should_translate else 0, 708 | data['comp']['height'] * 0.5 * self.scale_factor if should_translate else 0, 709 | 0 710 | ) 711 | ) 712 | 713 | if layer['type'] == 'camera': 714 | obj_data.sensor_fit = 'VERTICAL' 715 | self.import_property( 716 | slot_mgr=slot_mgr, 717 | obj=obj_data, 718 | ae_obj=layer, 719 | data_path='lens', 720 | data_index=-1, 721 | prop_data=layer['zoom']['channels'][0], 722 | comp_framerate=data['comp']['frameRate'], 723 | desired_framerate=desired_framerate, 724 | # 24 = default camera sensor height 725 | mul=24 / data['comp']['height'] 726 | ) 727 | 728 | imported_objects.append((transform_target, layer)) 729 | 730 | # Baked transforms include parent transforms 731 | if not data['transformsBaked']: 732 | for obj, layer in imported_objects: 733 | if layer['parentIndex'] is not None: 734 | obj.parent = innermost_objects_by_index[layer['parentIndex']] 735 | 736 | if self.create_new_collection: 737 | dst_collection = bpy.data.collections.new(data['comp']['name']) 738 | context.collection.children.link(dst_collection) 739 | else: 740 | dst_collection = context.collection 741 | 742 | for obj in added_objects: 743 | dst_collection.objects.link(obj) 744 | obj.select_set(True) 745 | 746 | context.view_layer.update() 747 | 748 | if self.use_comp_resolution: 749 | render_settings = context.scene.render 750 | render_settings.resolution_x = data['comp']['width'] 751 | render_settings.resolution_y = data['comp']['height'] 752 | 753 | pixel_aspect = data['comp']['pixelAspect'] 754 | # Check whether the pixel aspect ratio can be expressed precisely as a ratio of smallish integers 755 | pixel_aspect_frac = Fraction(pixel_aspect).limit_denominator(1000) 756 | if isclose(float(pixel_aspect_frac), pixel_aspect, abs_tol=1e-11): 757 | render_settings.pixel_aspect_x = pixel_aspect_frac.numerator 758 | render_settings.pixel_aspect_y = pixel_aspect_frac.denominator 759 | else: 760 | # Blender clamps pixel aspect X and Y to never go below 1 761 | if pixel_aspect > 1: 762 | render_settings.pixel_aspect_x = pixel_aspect 763 | render_settings.pixel_aspect_y = 1 764 | else: 765 | render_settings.pixel_aspect_x = 1 766 | render_settings.pixel_aspect_y = 1 / pixel_aspect 767 | 768 | if self.adjust_frame_start_end: 769 | # Compensate for floating-point error 770 | # TODO: there should be a lot less floating-point error. ExtendScript is probably printing floats poorly. 771 | # (Or maybe After Effects just uses floats instead of doubles like JS does) 772 | context.scene.frame_start = floor(data['comp']['workArea'][0] * desired_framerate + 1e-13) 773 | # After Effects' work area excludes the end point; Blender's includes it. Subtract 1 from the end. 774 | context.scene.frame_end = ceil(data['comp']['workArea'][1] * desired_framerate - 1e-13) - 1 775 | 776 | # Import switching between camera layers as markers 777 | if self.cameras_to_markers: 778 | # Keep track of existing markers to avoid adding new ones in the same place 779 | existing_markers: dict[int, TimelineMarker] = dict() 780 | for marker in context.scene.timeline_markers.values(): 781 | existing_markers[marker.frame] = marker 782 | 783 | camera_in_out_frames.sort() 784 | prev_enabled_camera = None 785 | 786 | # Check all the camera layer in/out points, since those are the only places a camera change can occur 787 | for frame in camera_in_out_frames: 788 | # Find the topmost camera layer 789 | enabled_camera = None 790 | for camera in cameras: 791 | if camera.inFrame <= frame and camera.outFrame > frame: 792 | enabled_camera = camera 793 | break 794 | 795 | # If the camera changed, add or update the marker at that frame 796 | if enabled_camera != prev_enabled_camera: 797 | prev_enabled_camera = enabled_camera 798 | if enabled_camera is None: 799 | continue 800 | marker = existing_markers.get(frame) 801 | if marker is None: 802 | marker = context.scene.timeline_markers.new(f'M_{enabled_camera.camera.name}', frame=frame) 803 | marker.camera = enabled_camera.camera 804 | 805 | return {'FINISHED'} 806 | 807 | def draw(self, context: 'bpy.types.Context'): 808 | layout = self.layout 809 | layout.use_property_split = True 810 | layout.use_property_decorate = False 811 | 812 | col = layout.column() 813 | col.prop(self, 'scale_factor') 814 | col.prop(self, 'handle_framerate') 815 | 816 | col = layout.column() 817 | # Give the checkboxes room to breathe. Ideally, this wouldn't be necessary, but the default width is just a few 818 | # pixels too small and truncates the labels. 819 | col.use_property_split = False 820 | col.prop(self, 'comp_center_to_origin') 821 | col.prop(self, 'use_comp_resolution') 822 | col.prop(self, 'create_new_collection') 823 | col.prop(self, 'adjust_frame_start_end') 824 | col.prop(self, 'cameras_to_markers') 825 | 826 | def menu_func_import(self, context): 827 | self.layout.operator(ImportAEComp.bl_idname, text="After Effects composition data, converted (.json)") 828 | 829 | def register(): 830 | bpy.utils.register_class(ImportAEComp) 831 | bpy.types.TOPBAR_MT_file_import.append(menu_func_import) 832 | 833 | def unregister(): 834 | bpy.utils.unregister_class(ImportAEComp) 835 | bpy.types.TOPBAR_MT_file_import.remove(menu_func_import) 836 | 837 | if __name__ == "__main__": 838 | register() 839 | --------------------------------------------------------------------------------