/*
Copyright © 2020-2024 adroitwhiz

Common Public Attribution License Version 1.0 (CPAL)

1. “Definitions”

1.0.1 “Commercial Use” means distribution or otherwise making the Covered Code available to a third party.

1.1 “Contributor” means each entity that creates or contributes to the creation of Modifications.

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.

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.

1.4 “Electronic Distribution Mechanism” means a mechanism generally accepted in the software development community for the electronic transfer of data.

1.5 “Executable” means Covered Code in any form other than Source Code.

1.6 “Initial Developer” means the individual or entity identified as the Initial Developer in the Source Code notice required by Exhibit A.

1.7 “Larger Work” means a work which combines Covered Code or portions thereof with code not governed by the terms of this License.

1.8 “License” means this document.

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.

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:

A. Any addition to or deletion from the contents of a file containing Original Code or previous Modifications.

B. Any new file that contains any part of the Original Code or previous Modifications.

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.

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.

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.

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.

2. Source Code License.

2.1 The Initial Developer Grant.
The Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license, subject to third party intellectual property claims:

(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

(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).

(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.

(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.

2.2 Contributor Grant.
Subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license

(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

(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).

(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.

(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.

3. Distribution Obligations.

3.1 Application of License.
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.

3.2 Availability of Source Code.
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.

3.3 Description of Modifications.
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.

3.4 Intellectual Property Matters

(a) Third Party Claims.
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.

(b) Contributor APIs.
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.

(c) Representations.
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.

3.5 Required Notices.
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.

3.6 Distribution of Executable Versions.
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.

3.7 Larger Works.
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.

4. Inability to Comply Due to Statute or Regulation.

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.

5. Application of this License.

This License applies to code to which the Initial Developer has attached the notice in Exhibit A and to related Covered Code.

6. Versions of the License.

6.1 New Versions.
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.

6.2 Effect of New Versions.
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.

6.3 Derivative Works.
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.)

7. DISCLAIMER OF WARRANTY.

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.

8. TERMINATION.

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.

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:

(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.

(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.

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.

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.

9. LIMITATION OF LIABILITY.

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.

10. U.S. GOVERNMENT END USERS.

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.

11. MISCELLANEOUS.

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.

12. RESPONSIBILITY FOR CLAIMS.

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.

13. MULTIPLE-LICENSED CODE.

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.

14. ADDITIONAL TERM: ATTRIBUTION

(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.

(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”).

(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.

(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.

15. ADDITIONAL TERM: NETWORK USE.

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.

EXHIBIT A. Common Public Attribution License Version 1.0.

“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.

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.

The Original Code is after-effects-to-blender-export.

The Original Developer is the Initial Developer.

The Initial Developer of the Original Code is adroitwhiz. All portions of the code written by adroitwhiz are Copyright (c) adroitwhiz. All Rights Reserved.

EXHIBIT B. Attribution Information

Attribution Copyright Notice: Copyright © 2020-2024 adroitwhiz. All Rights Reserved.

Attribution URL: https://github.com/adroitwhiz/after-effects-to-blender-export/

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.
*/

{
    // A collection of helpful utility functions, (mostly) shamelessly stolen from Rhubarb Lip Sync
// https://github.com/DanielSWolf/rhubarb-lip-sync
// Their license is as follows:
/*
Copyright (c) 2015-2016 Daniel Wolf

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

// Polyfill for Object.assign
"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<arguments.length;d++){var e=arguments[d];if(null!=e)for(var f in e)Object.prototype.hasOwnProperty.call(e,f)&&(c[f]=e[f])}return c});

// Polyfill for Array.isArray
Array.isArray||(Array.isArray=function(r){return"[object Array]"===Object.prototype.toString.call(r)});

// Polyfill for Array.prototype.map
Array.prototype.map||(Array.prototype.map=function(r){var t,n,o;if(null==this)throw new TypeError("this is null or not defined");var e=Object(this),i=e.length>>>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<i;){var a,p;o in e&&(a=e[o],p=r.call(t,a,o,e),n[o]=p),o++}return n});

// Polyfill for Array.prototype.forEach
Array.prototype.forEach||(Array.prototype.forEach=function(a,b){var c,d;if(null===this)throw new TypeError(" this is null or not defined");var e=Object(this),f=e.length>>>0;if("function"!=typeof a)throw new TypeError(a+" is not a function");for(arguments.length>1&&(c=b),d=0;d<f;){var g;d in e&&(g=e[d],a.call(c,g,d,e)),d++}});

// Polyfill for Array.prototype.indexOf
Array.prototype.indexOf||(Array.prototype.indexOf=function(r,t){var n;if(null==this)throw new TypeError('"this" is null or not defined');var e=Object(this),i=e.length>>>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<i;){if(n in e&&e[n]===r)return n;n++}return-1});

//  json2.js
//  2017-06-12
//  Public Domain.
//  NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.

//  USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
//  NOT CONTROL.

//  This file creates a global JSON object containing two methods: stringify
//  and parse. This file provides the ES5 JSON capability to ES3 systems.
//  If a project might run on IE8 or earlier, then this file should be included.
//  This file does nothing on ES5 systems.

//      JSON.stringify(value, replacer, space)
//          value       any JavaScript value, usually an object or array.
//          replacer    an optional parameter that determines how object
//                      values are stringified for objects. It can be a
//                      function or an array of strings.
//          space       an optional parameter that specifies the indentation
//                      of nested structures. If it is omitted, the text will
//                      be packed without extra whitespace. If it is a number,
//                      it will specify the number of spaces to indent at each
//                      level. If it is a string (such as "\t" or "&nbsp;"),
//                      it contains the characters used to indent at each level.
//          This method produces a JSON text from a JavaScript value.
//          When an object value is found, if the object contains a toJSON
//          method, its toJSON method will be called and the result will be
//          stringified. A toJSON method does not serialize: it returns the
//          value represented by the name/value pair that should be serialized,
//          or undefined if nothing should be serialized. The toJSON method
//          will be passed the key associated with the value, and this will be
//          bound to the value.

//          For example, this would serialize Dates as ISO strings.

//              Date.prototype.toJSON = function (key) {
//                  function f(n) {
//                      // Format integers to have at least two digits.
//                      return (n < 10)
//                          ? "0" + n
//                          : n;
//                  }
//                  return this.getUTCFullYear()   + "-" +
//                       f(this.getUTCMonth() + 1) + "-" +
//                       f(this.getUTCDate())      + "T" +
//                       f(this.getUTCHours())     + ":" +
//                       f(this.getUTCMinutes())   + ":" +
//                       f(this.getUTCSeconds())   + "Z";
//              };

//          You can provide an optional replacer method. It will be passed the
//          key and value of each member, with this bound to the containing
//          object. The value that is returned from your method will be
//          serialized. If your method returns undefined, then the member will
//          be excluded from the serialization.

//          If the replacer parameter is an array of strings, then it will be
//          used to select the members to be serialized. It filters the results
//          such that only members with keys listed in the replacer array are
//          stringified.

//          Values that do not have JSON representations, such as undefined or
//          functions, will not be serialized. Such values in objects will be
//          dropped; in arrays they will be replaced with null. You can use
//          a replacer function to replace those with JSON values.

//          JSON.stringify(undefined) returns undefined.

//          The optional space parameter produces a stringification of the
//          value that is filled with line breaks and indentation to make it
//          easier to read.

//          If the space parameter is a non-empty string, then that string will
//          be used for indentation. If the space parameter is a number, then
//          the indentation will be that many spaces.

//          Example:

//          text = JSON.stringify(["e", {pluribus: "unum"}]);
//          // text is '["e",{"pluribus":"unum"}]'

//          text = JSON.stringify(["e", {pluribus: "unum"}], null, "\t");
//          // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'

//          text = JSON.stringify([new Date()], function (key, value) {
//              return this[key] instanceof Date
//                  ? "Date(" + this[key] + ")"
//                  : value;
//          });
//          // text is '["Date(---current time---)"]'

//      JSON.parse(text, reviver)
//          This method parses a JSON text to produce an object or array.
//          It can throw a SyntaxError exception.

//          The optional reviver parameter is a function that can filter and
//          transform the results. It receives each of the keys and values,
//          and its return value is used instead of the original value.
//          If it returns what it received, then the structure is not modified.
//          If it returns undefined then the member is deleted.

//          Example:

//          // Parse the text. Values that look like ISO date strings will
//          // be converted to Date objects.

//          myData = JSON.parse(text, function (key, value) {
//              var a;
//              if (typeof value === "string") {
//                  a =
//   /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
//                  if (a) {
//                      return new Date(Date.UTC(
//                         +a[1], +a[2] - 1, +a[3], +a[4], +a[5], +a[6]
//                      ));
//                  }
//                  return value;
//              }
//          });

//          myData = JSON.parse(
//              "[\"Date(09/09/2001)\"]",
//              function (key, value) {
//                  var d;
//                  if (
//                      typeof value === "string"
//                      && value.slice(0, 5) === "Date("
//                      && value.slice(-1) === ")"
//                  ) {
//                      d = new Date(value.slice(5, -1));
//                      if (d) {
//                          return d;
//                      }
//                  }
//                  return value;
//              }
//          );

//  This is a reference implementation. You are free to copy, modify, or
//  redistribute.

/*jslint
    eval, for, this
*/

/*property
    JSON, apply, call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
    getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
    lastIndex, length, parse, prototype, push, replace, slice, stringify,
    test, toJSON, toString, valueOf
*/


// Create a JSON object only if one does not already exist. We create the
// methods in a closure to avoid creating global variables.

if (typeof JSON !== "object") {
    JSON = {};
}

(function () {
    "use strict";

    var rx_one = /^[\],:{}\s]*$/;
    var rx_two = /\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g;
    var rx_three = /"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
    var rx_four = /(?:^|:|,)(?:\s*\[)+/g;
    var rx_escapable = /[\\"\u0000-\u001f\u007f-\u009f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;
    var rx_dangerous = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g;

    function f(n) {
        // Format integers to have at least two digits.
        return (n < 10)
            ? "0" + n
            : n;
    }

    function this_value() {
        return this.valueOf();
    }

    if (typeof Date.prototype.toJSON !== "function") {

        Date.prototype.toJSON = function () {

            return isFinite(this.valueOf())
                ? (
                    this.getUTCFullYear()
                    + "-"
                    + f(this.getUTCMonth() + 1)
                    + "-"
                    + f(this.getUTCDate())
                    + "T"
                    + f(this.getUTCHours())
                    + ":"
                    + f(this.getUTCMinutes())
                    + ":"
                    + f(this.getUTCSeconds())
                    + "Z"
                )
                : null;
        };

        Boolean.prototype.toJSON = this_value;
        Number.prototype.toJSON = this_value;
        String.prototype.toJSON = this_value;
    }

    var gap;
    var indent;
    var meta;
    var rep;


    function quote(string) {

// If the string contains no control characters, no quote characters, and no
// backslash characters, then we can safely slap some quotes around it.
// Otherwise we must also replace the offending characters with safe escape
// sequences.

        rx_escapable.lastIndex = 0;
        return rx_escapable.test(string)
            ? "\"" + string.replace(rx_escapable, function (a) {
                var c = meta[a];
                return typeof c === "string"
                    ? c
                    : "\\u" + ("0000" + a.charCodeAt(0).toString(16)).slice(-4);
            }) + "\""
            : "\"" + string + "\"";
    }


    function str(key, holder) {

// Produce a string from holder[key].

        var i;          // The loop counter.
        var k;          // The member key.
        var v;          // The member value.
        var length;
        var mind = gap;
        var partial;
        var value = holder[key];

// If the value has a toJSON method, call it to obtain a replacement value.

        if (
            value
            && typeof value === "object"
            && typeof value.toJSON === "function"
        ) {
            value = value.toJSON(key);
        }

// If we were called with a replacer function, then call the replacer to
// obtain a replacement value.

        if (typeof rep === "function") {
            value = rep.call(holder, key, value);
        }

// What happens next depends on the value's type.

        switch (typeof value) {
        case "string":
            return quote(value);

        case "number":

// JSON numbers must be finite. Encode non-finite numbers as null.

            return (isFinite(value))
                ? String(value)
                : "null";

        case "boolean":
        case "null":

// If the value is a boolean or null, convert it to a string. Note:
// typeof null does not produce "null". The case is included here in
// the remote chance that this gets fixed someday.

            return String(value);

// If the type is "object", we might be dealing with an object or an array or
// null.

        case "object":

// Due to a specification blunder in ECMAScript, typeof null is "object",
// so watch out for that case.

            if (!value) {
                return "null";
            }

// Make an array to hold the partial results of stringifying this object value.

            gap += indent;
            partial = [];

// Is the value an array?

            if (Object.prototype.toString.apply(value) === "[object Array]") {

// The value is an array. Stringify every element. Use null as a placeholder
// for non-JSON values.

                length = value.length;
                for (i = 0; i < length; i += 1) {
                    partial[i] = str(i, value) || "null";
                }

// Join all of the elements together, separated with commas, and wrap them in
// brackets.

                v = partial.length === 0
                    ? "[]"
                    : gap
                        ? (
                            "[\n"
                            + gap
                            + partial.join(",\n" + gap)
                            + "\n"
                            + mind
                            + "]"
                        )
                        : "[" + partial.join(",") + "]";
                gap = mind;
                return v;
            }

// If the replacer is an array, use it to select the members to be stringified.

            if (rep && typeof rep === "object") {
                length = rep.length;
                for (i = 0; i < length; i += 1) {
                    if (typeof rep[i] === "string") {
                        k = rep[i];
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (
                                (gap)
                                    ? ": "
                                    : ":"
                            ) + v);
                        }
                    }
                }
            } else {

// Otherwise, iterate through all of the keys in the object.

                for (k in value) {
                    if (Object.prototype.hasOwnProperty.call(value, k)) {
                        v = str(k, value);
                        if (v) {
                            partial.push(quote(k) + (
                                (gap)
                                    ? ": "
                                    : ":"
                            ) + v);
                        }
                    }
                }
            }

// Join all of the member texts together, separated with commas,
// and wrap them in braces.

            v = partial.length === 0
                ? "{}"
                : gap
                    ? "{\n" + gap + partial.join(",\n" + gap) + "\n" + mind + "}"
                    : "{" + partial.join(",") + "}";
            gap = mind;
            return v;
        }
    }

// If the JSON object does not yet have a stringify method, give it one.

    if (typeof JSON.stringify !== "function") {
        meta = {    // table of character substitutions
            "\b": "\\b",
            "\t": "\\t",
            "\n": "\\n",
            "\f": "\\f",
            "\r": "\\r",
            "\"": "\\\"",
            "\\": "\\\\"
        };
        JSON.stringify = function (value, replacer, space) {

// The stringify method takes a value and an optional replacer, and an optional
// space parameter, and returns a JSON text. The replacer can be a function
// that can replace values, or an array of strings that will select the keys.
// A default replacer method can be provided. Use of the space parameter can
// produce text that is more easily readable.

            var i;
            gap = "";
            indent = "";

// If the space parameter is a number, make an indent string containing that
// many spaces.

            if (typeof space === "number") {
                for (i = 0; i < space; i += 1) {
                    indent += " ";
                }

// If the space parameter is a string, it will be used as the indent string.

            } else if (typeof space === "string") {
                indent = space;
            }

// If there is a replacer, it must be a function or an array.
// Otherwise, throw an error.

            rep = replacer;
            if (replacer && typeof replacer !== "function" && (
                typeof replacer !== "object"
                || typeof replacer.length !== "number"
            )) {
                throw new Error("JSON.stringify");
            }

// Make a fake root object containing our value under the key of "".
// Return the result of stringifying the value.

            return str("", {"": value});
        };
    }


// If the JSON object does not yet have a parse method, give it one.

    if (typeof JSON.parse !== "function") {
        JSON.parse = function (text, reviver) {

// The parse method takes a text and an optional reviver function, and returns
// a JavaScript value if the text is a valid JSON text.

            var j;

            function walk(holder, key) {

// The walk method is used to recursively walk the resulting structure so
// that modifications can be made.

                var k;
                var v;
                var value = holder[key];
                if (value && typeof value === "object") {
                    for (k in value) {
                        if (Object.prototype.hasOwnProperty.call(value, k)) {
                            v = walk(value, k);
                            if (v !== undefined) {
                                value[k] = v;
                            } else {
                                delete value[k];
                            }
                        }
                    }
                }
                return reviver.call(holder, key, value);
            }


// Parsing happens in four stages. In the first stage, we replace certain
// Unicode characters with escape sequences. JavaScript handles many characters
// incorrectly, either silently deleting them, or treating them as line endings.

            text = String(text);
            rx_dangerous.lastIndex = 0;
            if (rx_dangerous.test(text)) {
                text = text.replace(rx_dangerous, function (a) {
                    return (
                        "\\u"
                        + ("0000" + a.charCodeAt(0).toString(16)).slice(-4)
                    );
                });
            }

// In the second stage, we run the text against regular expressions that look
// for non-JSON patterns. We are especially concerned with "()" and "new"
// because they can cause invocation, and "=" because it can cause mutation.
// But just to be safe, we want to reject all unexpected forms.

// We split the second stage into 4 regexp operations in order to work around
// crippling inefficiencies in IE's and Safari's regexp engines. First we
// replace the JSON backslash pairs with "@" (a non-JSON character). Second, we
// replace all simple value tokens with "]" characters. Third, we delete all
// open brackets that follow a colon or comma or that begin the text. Finally,
// we look to see that the remaining characters are only whitespace or "]" or
// "," or ":" or "{" or "}". If that is so, then the text is safe for eval.

            if (
                rx_one.test(
                    text
                        .replace(rx_two, "@")
                        .replace(rx_three, "]")
                        .replace(rx_four, "")
                )
            ) {

// In the third stage we use the eval function to compile the text into a
// JavaScript structure. The "{" operator is subject to a syntactic ambiguity
// in JavaScript: it can begin a block or an object literal. We wrap the text
// in parens to eliminate the ambiguity.

                j = eval("(" + text + ")");

// In the optional fourth stage, we recursively walk the new structure, passing
// each name/value pair to a reviver function for possible transformation.

                return (typeof reviver === "function")
                    ? walk({"": j}, "")
                    : j;
            }

// If the text is not JSON parseable, then a SyntaxError is thrown.

            throw new SyntaxError("JSON.parse");
        };
    }
}());


// Object containing functions to create control description trees.
// For instance, `controls.StaticText({ text: 'Hello world' })`
// returns `{ __type__: StaticText, text: 'Hello world' }`.
var controlFunctions = (function() {
    var controlTypes = [
        // Strangely, 'dialog' and 'palette' need to start with a lower-case character
        ['Dialog', 'dialog'], ['Palette', 'palette'],
        'Panel', 'Group', 'TabbedPanel', 'Tab', 'Button', 'IconButton', 'Image', 'StaticText',
        'EditText', 'Checkbox', 'RadioButton', 'Progressbar', 'Slider', 'Scrollbar', 'ListBox',
        'DropDownList', 'TreeView', 'ListItem', 'FlashPlayer'
    ];
    var result = {};
    controlTypes.forEach(function(type){
        var isArray = Array.isArray(type);
        var key = isArray ? type[0] : type;
        var value = isArray ? type[1] : type;
        result[key] = function(options) {
            return Object.assign({ __type__: value }, options);
        };
    });
    return result;
})();

// ExtendScript's resource strings are a pain to write.
// This function allows them to be written in JSON notation, then converts them into the required
// format.
// For instance, this string: '{ "__type__": "StaticText", "text": "Hello world" }'
// is converted to this: 'StaticText { "text": "Hello world" }'.
// This code relies on the fact that, contrary to the language specification, all major JavaScript
// implementations keep object properties in insertion order.
function createResourceString(tree) {
    var result = JSON.stringify(tree, null, 2);
    result = result.replace(/(\{\s*)"__type__":\s*"(\w+)",?\s*/g, '$2 $1');
    return result;
}

function readTextFile(fileOrPath) {
    var filePath = fileOrPath.fsName || fileOrPath;
    var file = new File(filePath);
    function check() {
        if (file.error) throw new Error('Error reading file "' + filePath + '": ' + file.error);
    }
    try {
        file.open('r'); check();
        file.encoding = 'UTF-8'; check();
        var result = file.read(); check();
        return result;
    } finally {
        file.close(); check();
    }
}

function writeTextFile(fileOrPath, text) {
    var filePath = fileOrPath.fsName || fileOrPath;
    var file = new File(filePath);
    function check() {
        if (file.error) throw new Error('Error writing file "' + filePath + '": ' + file.error);
    }
    try {
        file.open('w'); check();
        file.encoding = 'UTF-8'; check();
        file.write(text); check();
    } finally {
        file.close(); check();
    }
}

function readSettingsFile(version) {
    try {
        var settings = JSON.parse(readTextFile(settingsFilePath));
        if (version === settings.version) return settings;
        return null;
    } catch (e) {
        return null;
    }
}

function writeSettingsFile(settings, version) {
    try {
        writeTextFile(settingsFilePath, JSON.stringify(Object.assign({}, settings, {version: version}), null, 2));
    } catch (e) {
        alert('Error persisting settings. ' + e.message);
    }
}


    var fileVersion = 3;
    var settingsVersion = '0.2';
    var settingsFilePath = Folder.userData.fullName + '/cam-export-settings.json';

    function showDialog(cb, opts) {
        var c = controlFunctions;
        var resourceString = createResourceString(
            c.Dialog({
                text: 'AE to Blender Export',
                settings: c.Group({
                    orientation: 'column',
                    alignment: ['fill', 'fill'],
                    alignChildren: ['left', 'top'],
                    saveDestination: c.Group({
                        alignment: ['fill', 'fill'],
                        label: c.StaticText({text: 'Save to'}),
                        savePath: c.EditText({
                            alignment: ['fill', 'fill'],
                            minimumSize: [400, 0]
                        }),
                        saveBrowse: c.Button({
                            properties: {name: 'browse'},
                            text: 'Browse',
                            alignment: ['right', 'right']
                        })
                    }),
                    timeRange: c.Group({
                        label: c.StaticText({text: 'Time range'}),
                        value: c.Group({
                            wholeComp: c.RadioButton({
                                text: 'Whole comp',
                                value: true
                            }),
                            workArea: c.RadioButton({
                                text: 'Work area'
                            }),
                            layerDuration: c.RadioButton({
                                text: 'Layer duration'
                            }),
                            // TODO: automatically detect per layer and channel whether to animate and when
                            /*automatic: c.RadioButton({
                                text: 'Automatic'
                            })*/
                        })
                    }),
                    selectedLayersOnly: c.Group({
                        label: c.StaticText({
                            text: 'Export selected layers only'
                        }),
                        value: c.Checkbox({
                            value: opts.selectionExists,
                            enabled: opts.selectionExists
                        })
                    }),
                    bakeTransforms: c.Group({
                        label: c.StaticText({
                            text: 'Bake transforms',
                            helpTip: "Calculate all transforms in After Effects. This may help if Blender is importing some transforms incorrectly."
                        }),
                        value: c.Checkbox({
                            value: opts.bakeTransforms,
                            enabled: opts.bakeTransforms
                        })
                    }),
                    frameSuperSampling: c.Group({
                        label: c.StaticText({
                            text: 'Transform sampling rate',
                            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.'
                        }),
                        value: c.EditText({
                            text: opts.frameSuperSampling,
                            minimumSize: [40, 0]
                        })
                    })
                }),
                separator: c.Group({ preferredSize: ['', 3] }),
                buttons: c.Group({
                    alignment: 'fill',
                    plug: c.Group({
                        text: c.StaticText({
                            text: 'Copyright © 2020-2024 adroitwhiz. All Rights Reserved.',
                            alignment: ['fill', 'center']
                        }),
                        link: c.Button({
                            text: '↗',
                            helpTip: 'Visit homepage',
                            maximumSize: [20, 20]
                        })
                    }),
                    doExport: c.Button({
                        properties: { name: 'ok' },
                        text: 'Export',
                        alignment: ['right', 'center']
                    }),
                    cancel: c.Button({
                        properties: { name: 'cancel' },
                        text: 'Cancel',
                        alignment: ['right', 'center']
                    })
                })
            })
        );

        var window = new Window(resourceString, 'Blender Export', undefined, {resizeable: false});

        var controls = {
            savePath: window.settings.saveDestination.savePath,
            saveBrowse: window.settings.saveDestination.saveBrowse,
            timeRange: {
                wholeComp: window.settings.timeRange.value.wholeComp,
                workArea: window.settings.timeRange.value.workArea,
                layerDuration: window.settings.timeRange.value.layerDuration,
                // automatic: window.settings.timeRange.value.automatic
            },
            selectedLayersOnly: window.settings.selectedLayersOnly,
            bakeTransforms: window.settings.bakeTransforms,
            frameSuperSampling: window.settings.frameSuperSampling,
            plugButton: window.buttons.plug.link,
            exportButton: window.buttons.doExport,
            cancelButton: window.buttons.cancel
        };

        window.onShow = function() {
            // Give uniform width to all labels
            var groups = toArray(window.settings.children);
            var labelWidths = groups.map(function(group) { return group.children[0].size.width; });
            var maxLabelWidth = Math.max.apply(Math, labelWidths);
            groups.forEach(function (group) {
                group.children[0].size.width = maxLabelWidth;
            });

            // Give uniform width to inputs
            var valueWidths = groups.map(function(group) {
                return last(group.children).bounds.right - group.children[1].bounds.left;
            });
            var maxValueWidth = Math.max.apply(Math, valueWidths);
            groups.forEach(function (group) {
                var multipleControls = group.children.length > 2;
                if (!multipleControls) {
                    group.children[1].size.width = maxValueWidth;
                }
            });

            window.layout.layout(true);
        };

        /*window.onResize = function() {
            window.layout.resize();
            window.layout.layout();
        }*/

        function getSettings() {
            var timeRange = null;
            for (var opt in controls.timeRange) {
                if (controls.timeRange.hasOwnProperty(opt) &&
                    controls.timeRange[opt].value) {
                        timeRange = opt;
                        break;
                    }
            }
            var frameSuperSampling = parseInt(controls.frameSuperSampling.value.text);
            if (isNaN(frameSuperSampling) || frameSuperSampling < 1 || frameSuperSampling > 128) {
                frameSuperSampling = 1;
            }
            return {
                savePath: controls.savePath.text,
                timeRange: timeRange,
                selectedLayersOnly: controls.selectedLayersOnly.value.value,
                frameSuperSampling: frameSuperSampling,
                bakeTransforms: controls.bakeTransforms.value.value
            };
        }

        /**
         * Apply previously-saved settings to the newly-created dialog
         */
        function applySettings(settings) {
            if (!settings) return;
            controls.savePath.text = settings.savePath;
            for (var button in controls.timeRange) {
                if (!controls.timeRange.hasOwnProperty(button)) continue;
                controls.timeRange[button].value = button === settings.timeRange;
            }
            controls.bakeTransforms.value.value = settings.bakeTransforms;
            if (typeof settings.frameSuperSampling === 'number') {
                controls.frameSuperSampling.value.text = settings.frameSuperSampling;
            }
        }

        controls.plugButton.onClick = function() {
            var url = 'https://github.com/adroitwhiz/after-effects-to-blender-export';
            system.callSystem(($.os.indexOf('Win') !== -1 ? 'explorer' : 'open') + ' "' + url + '"');
        }

        controls.saveBrowse.onClick = function() {
            var savePath;
            // Use existing file path if set
            if (controls.savePath.text !== '') {
                savePath = new File(controls.savePath.text).saveDlg('Choose the path and name for the layer data file', 'Layer data:*.json').fsName;
            } else {
                savePath = File.saveDialog('Choose the path and name for the layer data file', 'Layer data:*.json').fsName;
            }
            // MacOS doesn't support a filter in the save dialog, so manually override the extension to .json
            if (!/\.json$/.test(savePath)) {
                savePath = savePath.replace(/(\.\w+)?$/, '.json');
            }
            controls.savePath.text = savePath;
        }

        controls.exportButton.onClick = function() {
            try {
                cb(getSettings());
                writeSettingsFile(getSettings(), settingsVersion);
            } catch (err) {
                showBugReportWindow(err);
            }
            window.close();
        };

        applySettings(readSettingsFile(settingsVersion));

        return window;
    }

    function showBugReportWindow(err) {
        var c = controlFunctions;
        var resourceString = createResourceString(
            c.Dialog({
                text: 'Error',
                header: c.StaticText({
                    text: 'The script has encountered an error. You can report it and paste the error message below into the report:',
                    alignment: ['left', 'top']
                }),
                errorInfo: c.EditText({
                    alignment: ['fill', 'fill'],
                    minimumSize: [400, 100],
                    properties: {multiline: true}
                }),
                separator: c.Group({ preferredSize: ['', 3] }),
                buttons: c.Group({
                    close: c.Button({
                        properties: { name: 'close' },
                        text: 'Close'
                    }),
                    alignment: 'right',
                    report: c.Button({
                        properties: { name: 'report' },
                        text: 'Report Bug',
                        active: true
                    })
                })
            })
        );

        var window = new Window(resourceString, 'Error', undefined, {resizeable: false});
        window.errorInfo.text = err.message;

        window.buttons.report.onClick = function() {
            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';
            system.callSystem(($.os.indexOf('Win') !== -1 ? 'explorer' : 'open') + ' "' + url + '"');
        }

        window.buttons.close.onClick = function() {
            window.close();
        };

        window.show();
    }

    function checkPermissions() {
        if (app.preferences.getPrefAsLong('Main Pref Section', 'Pref_SCRIPTING_FILE_NETWORK_SECURITY') === 1) {
            return true;
        }
        var c = controlFunctions;
        var resourceString = createResourceString(
            c.Dialog({
                text: 'Error',
                header: c.StaticText({
                    text: 'The "Allow Scripts to Write Files and Access Network" setting is disabled, and must be enabled in order for this script to work.',
                    alignment: ['left', 'top']
                }),
                separator: c.Group({ preferredSize: ['', 3] }),
                buttons: c.Group({
                    alignment: 'right',
                    close: c.Button({
                        properties: { name: 'close' },
                        text: 'Close'
                    }),
                    openSettings: c.Button({
                        properties: { name: 'openSettings' },
                        text: 'Open Script Settings',
                        active: true
                    })
                })
            })
        );

        var window = new Window(resourceString, 'Error', undefined, {resizeable: false});

        window.buttons.openSettings.onClick = function() {
            // Open the Scripting and Expressions settings dialog (command #3131, as documented... nowhere)
            // This won't work if called inside the callback for some reason (thanks Adobe!) so use scheduleTask
            app.scheduleTask("app.executeCommand(3131)", 0, false);
            window.close();
        }

        window.buttons.close.onClick = function() {
            window.close();
        }

        window.show();
        return false;
    }

    function getCompositionViewer() {
        var project = app.project;
        if (!project) {
            throw new Error("Project does not exist.");
        }

        var activeViewer = app.activeViewer;
        if (activeViewer.type !== ViewerType.VIEWER_COMPOSITION) {
            throw new Error('Switch to the composition viewer.');
        }

        return activeViewer;
    }

    function main() {
        if (!checkPermissions()) return;
        getCompositionViewer().setActive();
        var activeComp = app.project.activeItem;
        if (!activeComp) {
            // This is the case if e.g. you've just created the project and the two big "New Composition" and
            // "New Composition From Footage" are showing where the composition viewer would be. The active viewer
            // is of ViewerType.VIEWER_COMPOSITION, but there's no actual composition open.
            throw new Error('No composition is currently open.');
        }

        var d = showDialog(
            function(settings) {
                runExport(settings, {
                    activeComp: activeComp
                })
            },
            {
                selectionExists: activeComp.selectedLayers.length > 0,
                frameSuperSampling: 1
            }
        );
        d.window.show();
    }

    function runExport(settings, opts) {
        var activeComp = opts.activeComp;
        var layersToExport = [];
        if (settings.selectedLayersOnly) {
            var layerIndicesMarkedForExport = {};
            for (var i = 0; i < activeComp.selectedLayers.length; i++) {
                var layer = activeComp.selectedLayers[i];
                layersToExport.push(layer);
                layerIndicesMarkedForExport[layer.index] = true;

                // If not baking transforms (also baking parents' transforms into child layers),
                // make sure to export all the selected layers' parents as well so that the children
                // can have the parent transforms applied to them
                if (!settings.bakeTransforms) {
                    var parent = layer.parent;
                    while (parent) {
                        if (!(parent.index in layerIndicesMarkedForExport)) {
                            layersToExport.push(parent);
                            layerIndicesMarkedForExport[parent.index] = true;
                        }
                        parent = parent.parent;
                    }
                }
            }
        } else {
            for (var i = 1; i <= activeComp.layers.length; i++) {
                var layer = activeComp.layers[i];
                layersToExport.push(layer);
            }
        }

        var json = {
            layers: [],
            sources: [],
            comp: {
                width: activeComp.width,
                height: activeComp.height,
                name: activeComp.name,
                pixelAspect: activeComp.pixelAspect,
                frameRate: activeComp.frameRate,
                workArea: [activeComp.workAreaStart, activeComp.workAreaDuration + activeComp.workAreaStart]
            },
            transformsBaked: settings.bakeTransforms,
            version: fileVersion
        };

        var exportedSources = [];

        function unenum(val) {
            switch (val) {
                case KeyframeInterpolationType.LINEAR: return 'linear';
                case KeyframeInterpolationType.BEZIER: return 'bezier';
                case KeyframeInterpolationType.HOLD: return 'hold';
            }

            throw new Error('Could not un-enum ' + val);
        }

        function startAndEndFrame(layer) {
            var startTime, duration;
            switch (settings.timeRange) {
                case 'workArea':
                    startTime = activeComp.workAreaStart;
                    duration = activeComp.workAreaDuration;
                    break;
                case 'layerDuration':
                    startTime = layer.inPoint;
                    duration = layer.outPoint - layer.inPoint;
                    break;
                case 'wholeComp':
                default:
                    startTime = 0;
                    duration = activeComp.duration;
                    break;
            }
            // avoid floating point weirdness by rounding, just in case
            var startFrame = Math.floor(startTime * activeComp.frameRate);
            var endFrame = startFrame + Math.ceil(duration * activeComp.frameRate);
            return [startFrame, endFrame];
        }

        function escapeStringForLiteral(str) {
            return str.replace(/(\\|")/g, '\\$1');
        }

        if (settings.bakeTransforms) {
            // Adding a layer deselects all others, so save the original selection here.
            var selectedLayers = [];
            for (var i = 0; i < activeComp.selectedLayers.length; i++) {
                selectedLayers.push(activeComp.selectedLayers[i]);
            }

            // `toWorld` only works inside expressions, so add a null object whose expression we will set and then evaluate
            // using `valueAtTime`.
            var evaluator = activeComp.layers.addNull();
            // Move the evaluator layer to the bottom to avoid messing up expressions which rely on layer indices
            evaluator.moveToEnd();
            // Adding a new effect invalidates references to all other effects in the stack, so create all effects first
            // before obtaining references to them. I thought JS was a garbage-collected language, Adobe!
            for (var i = 0; i < 4; i++) {
                evaluator.property("Effects").addProperty("ADBE Point3D Control");
            }
            var evalPoint1 = evaluator.property("Effects").property(1);
            var evalPoint2 = evaluator.property("Effects").property(2);
            var evalPoint3 = evaluator.property("Effects").property(3);
            var evalPoint4 = evaluator.property("Effects").property(4);
        }

        // Takes as input a list of 4 transformed points and returns the 3x4 affine transformation matrix that applies
        // such a transform. Assumes that the "source" points are [0, 0, 0], [1, 0, 0], [0, 1, 0], and [0, 0, 1].
        // The 4th row will always be [0, 0, 0, 1], and is thus omitted.
        function pointsToAffineMatrix(p0, p1, p2, p3) {
            return [
                p1[0] - p0[0],
                p2[0] - p0[0],
                p3[0] - p0[0],
                p0[0],
                p1[1] - p0[1],
                p2[1] - p0[1],
                p3[1] - p0[1],
                p0[1],
                p1[2] - p0[2],
                p2[2] - p0[2],
                p3[2] - p0[2],
                p0[2]
            ];
        };

        function exportBakedTransform(layer) {
            // It's possible to construct a 3D affine transform matrix given a mapping from 4 source points to 4 destination points.
            // The source points are the arguments of the toWorld functions, and the destination points are their results.
            // We calculate this affine transform matrix once per frame then decompose it on the Blender side.
            evalPoint1.property(1).expression = "thisComp.layer(\"" + escapeStringForLiteral(layer.name) + "\").toWorld([0, 0, 0])";
            evalPoint2.property(1).expression = "thisComp.layer(\"" + escapeStringForLiteral(layer.name) + "\").toWorld([1, 0, 0])";
            evalPoint3.property(1).expression = "thisComp.layer(\"" + escapeStringForLiteral(layer.name) + "\").toWorld([0, 1, 0])";
            evalPoint4.property(1).expression = "thisComp.layer(\"" + escapeStringForLiteral(layer.name) + "\").toWorld([0, 0, 1])";

            // Bake keyframe data
            var startEnd = startAndEndFrame(layer);
            var startFrame = startEnd[0];
            var endFrame = startEnd[1];

            var keyframes = [];

            for (var i = 0, n = (endFrame - startFrame) * settings.frameSuperSampling; i < n; i++) {
                var frame = (i / settings.frameSuperSampling) + startFrame;
                var time =  frame / activeComp.frameRate;
                var point1Val = evalPoint1.property(1).valueAtTime(time, false /* preExpression */);
                var point2Val = evalPoint2.property(1).valueAtTime(time, false /* preExpression */);
                var point3Val = evalPoint3.property(1).valueAtTime(time, false /* preExpression */);
                var point4Val = evalPoint4.property(1).valueAtTime(time, false /* preExpression */);
                var matrix = pointsToAffineMatrix(
                        point1Val,
                        point2Val,
                        point3Val,
                        point4Val
                );
                keyframes.push(matrix);
            }

            return {
                startFrame: startFrame,
                keyframes: keyframes,
                supersampling: settings.frameSuperSampling
            }
        }

        function exportProperty(prop, layer, exportedProp, channelOffset) {
            var valType = prop.propertyValueType;
            // The ternary conditional operator is left-associative in ExtendScript. HATE. HATE. HATE. HATE. HATE. HATE. HATE. HATE.
            var numDimensions = (valType === PropertyValueType.ThreeD || valType === PropertyValueType.ThreeD_SPATIAL ? 3 :
                (valType === PropertyValueType.TwoD || valType === PropertyValueType.TwoD_SPATIAL ? 2 :
                    1));

            if (prop.isSeparationFollower && numDimensions > 1) {
                throw new Error('Separation follower cannot have more than 1 dimension');
            }

            if (typeof exportedProp === 'undefined') {
                exportedProp = {numDimensions: numDimensions, channels: []};
                for (var i = 0; i < numDimensions; i++) {
                    exportedProp.channels.push({});
                }
            }

            if (prop.isSeparationLeader && prop.dimensionsSeparated) {
                for (var i = 0; i < numDimensions; i++) {
                    var dimProp = prop.getSeparationFollower(i);
                    exportProperty(dimProp, layer, exportedProp, i);
                }

                return exportedProp;
            }

            if (typeof channelOffset === 'undefined') channelOffset = 0;

            if (prop.isTimeVarying) {
                if (
                    (!prop.expressionEnabled) &&
                    (valType === PropertyValueType.ThreeD ||
                    valType === PropertyValueType.TwoD ||
                    valType === PropertyValueType.OneD)
                ) {
                    // Export keyframe Bezier data directly
                    for (var i = 0; i < numDimensions; i++) {
                        exportedProp.channels[i + channelOffset].isKeyframed = true;
                        exportedProp.channels[i + channelOffset].keyframesFormat = 'bezier';
                        exportedProp.channels[i + channelOffset].keyframes = [];
                    }

                    for (var keyIndex = 1; keyIndex <= prop.numKeys; keyIndex++) {
                        var time = prop.keyTime(keyIndex);
                        var value = prop.keyValue(keyIndex);
                        var easeIn = prop.keyInTemporalEase(keyIndex);
                        var easeOut = prop.keyOutTemporalEase(keyIndex);
                        var interpolationIn = unenum(prop.keyInInterpolationType(keyIndex));
                        var interpolationOut = unenum(prop.keyOutInterpolationType(keyIndex));
                        for (var i = 0; i < numDimensions; i++) {
                            exportedProp.channels[i + channelOffset].keyframes.push({
                                value: Array.isArray(value) ? value[i] : value,
                                easeIn: {
                                    speed: easeIn[i].speed,
                                    influence: easeIn[i].influence
                                },
                                easeOut: {
                                    speed: easeOut[i].speed,
                                    influence: easeOut[i].influence
                                },
                                time: prop.keyTime(keyIndex),
                                interpolationIn: interpolationIn,
                                interpolationOut: interpolationOut
                            })
                        }
                    }
                } else {
                    // Bake keyframe data
                    var startEnd = startAndEndFrame(layer);
                    var startFrame = startEnd[0];
                    var endFrame = startEnd[1];

                    for (var i = 0; i < numDimensions; i++) {
                        exportedProp.channels[i + channelOffset].isKeyframed = true;
                        exportedProp.channels[i + channelOffset].keyframesFormat = 'calculated';
                        exportedProp.channels[i + channelOffset].startFrame = startFrame;
                        exportedProp.channels[i + channelOffset].keyframes = [];
                        exportedProp.channels[i + channelOffset].supersampling = settings.frameSuperSampling;
                    }

                    for (var i = 0, n = (endFrame - startFrame) * settings.frameSuperSampling; i < n; i++) {
                        var frame = (i / settings.frameSuperSampling) + startFrame;
                        var time =  frame / activeComp.frameRate;
                        var propVal = prop.valueAtTime(time, false /* preExpression */);
                        for (var j = 0; j < numDimensions; j++) {
                            exportedProp.channels[j + channelOffset].keyframes.push(Array.isArray(propVal) ? propVal[j] : propVal);
                        }
                    }
                }
            } else {
                for (var i = 0; i < numDimensions; i++) {
                    exportedProp.channels[i + channelOffset].isKeyframed = false;
                    exportedProp.channels[i + channelOffset].value = Array.isArray(prop.value) ? prop.value[i] : prop.value;
                }
            }

            return exportedProp;
        }

        function exportSource(source) {
            var exportedSource = {
                height: source.height,
                width: source.width,
                name: source.name
            };
            if (source instanceof FootageItem) {
                if (source.mainSource instanceof SolidSource) {
                    exportedSource.type = 'solid';
                    exportedSource.color = source.mainSource.color;
                } else if (source.mainSource instanceof FileSource) {
                    exportedSource.type = 'file';
                    exportedSource.file = source.mainSource.file.absoluteURI;
                } else {
                    exportedSource.type = 'unknown';
                }
            } else {
                exportedSource.type = 'unknown';
            }
            return exportedSource;
        }

        function exportLayer (layer) {
            var layerType;
            if (layer instanceof CameraLayer) {
                layerType = 'camera';
            } else if (layer instanceof AVLayer) {
                layerType = 'av';
            } else {
                layerType = 'unknown';
            }

            var exportedObject = {
                name: layer.name,
                type: layerType,
                index: layer.index,
                parentIndex: layer.parent ? layer.parent.index : null,
                inFrame: layer.inPoint * activeComp.frameRate,
                outFrame: layer.outPoint * activeComp.frameRate,
                enabled: layer.enabled
            };

            if (settings.bakeTransforms) {
                exportedObject.transform = exportBakedTransform(layer);
            } else {
                exportedObject.position = exportProperty(layer.position, layer);
                exportedObject.rotationX = exportProperty(layer.xRotation, layer);
                exportedObject.rotationY = exportProperty(layer.yRotation, layer);
                exportedObject.rotationZ = exportProperty(layer.rotation, layer);
                exportedObject.orientation = exportProperty(layer.orientation, layer);
            }

            if (layer instanceof CameraLayer) {
                exportedObject.zoom = exportProperty(layer.zoom, layer);
            }

            // The "Point of Interest" property exists and is not hidden, meaning it's taking effect
            // Interestingly, `pointOfInterest.canSetExpression` is always true for other layer types
            if (
                (layer instanceof CameraLayer || layer instanceof LightLayer) &&
                layer.pointOfInterest.canSetExpression &&
                !settings.bakeTransforms
            ) {
                exportedObject.pointOfInterest = exportProperty(layer.pointOfInterest, layer);
            }

            if (layer instanceof AVLayer) {
                // Export layer source
                var alreadyExported = false;
                for (var i = 0; i < exportedSources.length; i++) {
                    if (exportedSources[i] === layer.source) {
                        alreadyExported = true;
                        break;
                    }
                }
                if (!alreadyExported) {
                    exportedSources.push(layer.source);
                    json.sources.push(exportSource(layer.source));
                }
                exportedObject.source = exportedSources.indexOf(layer.source);

                if (!settings.bakeTransforms) {
                    exportedObject.anchorPoint = exportProperty(layer.anchorPoint, layer);
                    exportedObject.scale = exportProperty(layer.scale, layer);
                }
                exportedObject.opacity = exportProperty(layer.opacity, layer);
                exportedObject.nullLayer = layer.nullLayer;
            }

            return exportedObject;
        }

        try {
            for (var j = 0; j < layersToExport.length; j++) {
                try {
                    var exportedLayer = exportLayer(layersToExport[j]);
                    json.layers.push(exportedLayer);
                } catch (err) {
                    // Give specific information on what layer is causing the problem
                    // This allows the user to fix it by deselecting the layer, and makes debugging easier
                    throw new Error('Error exporting layer "' + layersToExport[j].name + '"\nOn line ' + err.line + ': ' + err.message);
                }
            }
        } finally {
            if (settings.bakeTransforms) {
                evaluator.remove();
                for (var i = 0; i < selectedLayers.length; i++) {
                    selectedLayers[i].selected = true;
                }
            }
        }

        var savePath = settings.savePath.replace(/\.\w+$/, '.json');
        writeTextFile(savePath, JSON.stringify(json, null, 2));
    }

    try {
        // If we can't get the composition viewer, fail early rather than baiting the user into filling out all the
        // settings in the dialog just for it to fail when they click Export
        getCompositionViewer();
        main();
    } catch (err) {
        alert(err.message, 'Error');
    }
}