// Rig Kit 1.1 for EffectCraft
//
// A character rigging and animation panel: IK chains, Bezier IK, controllers, bones,
// automations, keyframe helpers and camera rigs, in one dockable panel.
//
// 1.1: rigs render about three times faster (a full character with tail: about 120 ms a frame
// down to about 35); 3D IK chains work whichever way they face; locked controllers, re-parented
// chains and duplicated comps give the right pose.
//
// Install: File > Scripts > Install ScriptUI Panel... and pick this file; it is then listed at the
// bottom of the Window menu. Or run it once with File > Scripts > Run Script File... to get it as a
// floating palette.
//
// Written for EffectCraft 0.10 (its ScriptUI, scripting object model and expression engine).
// RK.NAME below renames the panel, the layers it names after itself and its settings section.

(function rigKitPanel(thisObj) {
"use strict";

var RK = {};
RK.NAME = "Rig Kit";
RK.VERSION = "1.1";
RK.SETTINGS = "RigKit";

// ============================================================================ utilities

// ScriptUIGraphics takes colours as linear light: convert sRGB (0-255) once, here.
RK.lin = function (v) {
    return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
};
RK.rgb = function (r, g, b, a) {
    return [RK.lin(r / 255), RK.lin(g / 255), RK.lin(b / 255), a === undefined ? 1 : a];
};

RK.C = {
    icon: RK.rgb(128, 186, 255),      // icons
    active: RK.rgb(229, 32, 32),      // the tab shown, hovered and checked icons
    activeText: RK.rgb(236, 24, 24),  // hovered and checked labels
    rule: RK.rgb(135, 135, 135, 0.45), // separators
    hover: RK.rgb(255, 255, 255, 0.07),
    press: RK.rgb(255, 255, 255, 0.13),
    dim: RK.rgb(135, 135, 135),       // disabled icons and labels
    faint: RK.rgb(135, 135, 135, 0.45),
    white: RK.rgb(235, 235, 235),
    logoA: RK.rgb(235, 235, 235),
    logoB: RK.rgb(229, 32, 32)
};

// Inter Regular advance widths (thousandths of an em) for the characters 32 to 126: EffectCraft
// draws ScriptUI text in Inter, and centring text needs its real width.
RK.INTER = [281, 288, 466, 633, 642, 982, 644, 300, 365, 365, 501, 662, 288, 460, 288, 360, 631, 407, 610,
    618, 646, 593, 620, 566, 619, 620, 288, 302, 662, 662, 662, 511, 966, 690, 654, 730, 722, 601, 590, 746,
    743, 269, 571, 672, 565, 903, 753, 765, 639, 765, 644, 642, 646, 744, 690, 985, 682, 679, 629, 365, 360,
    365, 471, 456, 323, 562, 612, 571, 612, 583, 370, 613, 591, 242, 242, 549, 242, 876, 591, 600, 612, 612,
    376, 528, 327, 591, 562, 818, 546, 562, 552, 426, 333, 426, 662];

RK.textWidth = function (s, size) {
    var w = 0;
    s = String(s);
    for (var i = 0; i < s.length; i++) {
        var c = s.charCodeAt(i);
        w += c >= 32 && c <= 126 ? RK.INTER[c - 32] : 600;
    }
    return w * (size || 12) / 1000;
};

RK.trim = function (s) {
    return String(s).replace(/^\s+|\s+$/g, "");
};

// Cut a message into lines of at most n characters, at spaces.
RK.wrap = function (s, n) {
    var out = [], paras = String(s).split("\n");
    for (var p = 0; p < paras.length; p++) {
        var words = paras[p].split(" "), line = "";
        for (var i = 0; i < words.length; i++) {
            var t = line ? line + " " + words[i] : words[i];
            if (t.length > n && line) {
                out.push(line);
                line = words[i];
            } else line = t;
        }
        out.push(line);
    }
    return out.join("\n");
};

RK.round = function (v, d) {
    var k = Math.pow(10, d === undefined ? 2 : d);
    return Math.round(v * k) / k;
};

RK.clone = function (a) {
    return a instanceof Array ? a.slice() : a;
};

// ---------------------------------------------------------------------------- settings

RK.prefCache = null;
RK.prefs = function () {
    if (RK.prefCache) return RK.prefCache;
    var o = {};
    try {
        if (app.settings.haveSetting(RK.SETTINGS, "prefs")) o = JSON.parse(app.settings.getSetting(RK.SETTINGS, "prefs")) || {};
    } catch (e) {
        o = {};
    }
    RK.prefCache = o;
    return o;
};
RK.pref = function (key, def) {
    var o = RK.prefs();
    return o.hasOwnProperty(key) ? o[key] : def;
};
RK.setPref = function (key, value) {
    var o = RK.prefs();
    o[key] = value;
    try {
        app.settings.saveSetting(RK.SETTINGS, "prefs", JSON.stringify(o));
    } catch (e) {}
};

// ============================================================================ icons
//
// Each icon is a list of drawing operations in its own box (tab icons 20 x 18, tool icons
// 24 x 16), stroked or filled in one colour:
//   ["L", x1, y1, x2, y2, ...]   polyline        ["Z", x1, y1, ...]  closed polyline
//   ["C", cx, cy, r]             circle outline  ["D", cx, cy, r]    filled disc
//   ["R", x, y, w, h]            rectangle       ["F", x, y, w, h]   filled rectangle
//   ["P", x1, y1, x2, y2, ...]   filled polygon  ["T", text, x, y, size]  text
//   ["E", cx, cy, rx, ry]        ellipse outline
// All strokes of an icon go into one path and all fills into another, so an icon costs two
// draw operations whatever its shape.

RK.I = {};

// Points of a cubic Bezier as a flat list, for "L" operations.
RK.bez = function (x0, y0, x1, y1, x2, y2, x3, y3, n) {
    var out = [];
    n = n || 10;
    for (var i = 0; i <= n; i++) {
        var t = i / n, u = 1 - t;
        out.push(u * u * u * x0 + 3 * u * u * t * x1 + 3 * u * t * t * x2 + t * t * t * x3);
        out.push(u * u * u * y0 + 3 * u * u * t * y1 + 3 * u * t * t * y2 + t * t * t * y3);
    }
    return out;
};
// Points of a circular arc (degrees, clockwise on screen from the x axis).
RK.arc = function (cx, cy, r, a0, a1, n) {
    var out = [];
    n = n || 12;
    for (var i = 0; i <= n; i++) {
        var a = (a0 + (a1 - a0) * i / n) * Math.PI / 180;
        out.push(cx + r * Math.cos(a), cy + r * Math.sin(a));
    }
    return out;
};
RK.op = function (kind, pts) {
    return [kind].concat(pts);
};

// ---- tab icons (20 x 18)
RK.I.tabRig = [
    ["C", 10, 3, 2.2],
    ["L", 10, 5.2, 10, 10.5],
    ["L", 4.5, 4.5, 7, 8, 10, 7, 13, 8, 15.5, 11.5],
    ["L", 6, 17, 7.5, 13.5, 10, 10.5, 12.5, 13.5, 15, 17],
    ["D", 7, 8, 1.1], ["D", 13, 8, 1.1], ["D", 7.5, 13.5, 1.1], ["D", 12.5, 13.5, 1.1]
];
RK.I.tabAuto = (function () {
    var ops = [["C", 10, 9, 4.6], ["C", 10, 9, 1.7]];
    for (var i = 0; i < 8; i++) {
        var a = i * Math.PI / 4;
        ops.push(["L", 10 + 5.2 * Math.cos(a), 9 + 5.2 * Math.sin(a), 10 + 8 * Math.cos(a), 9 + 8 * Math.sin(a)]);
    }
    return ops;
})();
RK.I.tabAnim = [
    RK.op("L", RK.bez(3.5, 14.5, 11, 14.5, 9, 3.5, 16.5, 3.5, 12)),
    ["P", 3.5, 11.5, 6.5, 14.5, 3.5, 17.5, 0.5, 14.5],
    ["P", 16.5, 0.5, 19.5, 3.5, 16.5, 6.5, 13.5, 3.5]
];
RK.I.tabCam = [
    ["R", 1.5, 7, 11, 8],
    ["Z", 12.5, 9.5, 18.5, 6.5, 18.5, 15.5, 12.5, 12.5],
    ["C", 4.5, 3.8, 2.3], ["C", 10, 3.8, 2.3]
];
RK.I.tabSettings = [
    ["L", 1.5, 4, 18.5, 4], ["L", 1.5, 9, 18.5, 9], ["L", 1.5, 14, 18.5, 14],
    ["F", 11, 1.5, 3, 5], ["F", 4.5, 6.5, 3, 5], ["F", 13.5, 11.5, 3, 5]
];

// ---- tool icons (24 x 16)
RK.I.autorig = [
    ["C", 12, 2.6, 2.1],
    ["L", 12, 4.7, 12, 9.5],
    ["L", 6.5, 3.5, 9, 7, 12, 6, 15, 7, 17.5, 10.5],
    ["L", 8, 15.5, 9.5, 12.5, 12, 9.5, 14.5, 12.5, 17, 15.5],
    ["D", 9, 7, 1], ["D", 15, 7, 1], ["D", 9.5, 12.5, 1], ["D", 14.5, 12.5, 1]
];
RK.I.ik = [
    ["L", 2.5, 13.5, 9, 3.5, 16, 10],
    ["C", 2.5, 13.5, 1.6], ["D", 9, 3.5, 1.6],
    ["C", 19.5, 11.5, 3.4], ["D", 19.5, 11.5, 1]
];
RK.I.ikgoal = [
    ["C", 15, 8, 5.5], ["D", 15, 8, 1.5],
    ["L", 1.5, 8, 8.5, 8], ["L", 5.5, 5, 8.5, 8, 5.5, 11]
];
RK.I.bezierik = [
    RK.op("L", RK.bez(2, 14, 7, 3, 17, 3, 22, 14, 14)),
    ["L", 2, 14, 6.5, 2.5], ["L", 22, 14, 17.5, 2.5],
    ["F", 5, 1, 3, 3], ["F", 16, 1, 3, 3],
    ["D", 2, 14, 1.4], ["D", 22, 14, 1.4]
];
RK.I.rotmorph = [
    RK.op("L", RK.arc(12, 8, 6.2, -40, 220, 16)),
    ["L", 14.5, 1.2, 16.8, 4.1, 13.2, 5.2],
    ["P", 12, 5.6, 14.4, 8, 12, 10.4, 9.6, 8]
];
RK.I.controllers = [
    ["C", 12, 8, 4.6], ["D", 12, 8, 1.2],
    ["L", 12, 0.5, 12, 2.6], ["L", 12, 13.4, 12, 15.5], ["L", 4.5, 8, 6.6, 8], ["L", 17.4, 8, 19.5, 8]
];
RK.I.bones = [
    ["C", 4.2, 5, 2.1], ["C", 4.2, 11, 2.1], ["C", 19.8, 5, 2.1], ["C", 19.8, 11, 2.1],
    ["L", 6.2, 6.6, 17.8, 6.6], ["L", 6.2, 9.4, 17.8, 9.4]
];
RK.I.zero = [
    ["E", 12, 8, 3.6, 5.6],
    ["L", 9, 14.5, 15, 1.5],
    ["L", 5, 2, 3, 2, 3, 14, 5, 14], ["L", 19, 2, 21, 2, 21, 14, 19, 14]
];
RK.I.list = [
    ["D", 4.5, 3.5, 1.3], ["D", 4.5, 8, 1.3], ["D", 4.5, 12.5, 1.3],
    ["L", 8, 3.5, 20, 3.5], ["L", 8, 8, 20, 8], ["L", 8, 12.5, 17, 12.5],
    ["L", 19.5, 10.5, 19.5, 15.5], ["L", 17, 13, 22, 13]
];
RK.I.rename = [
    ["T", "Ab", 1.5, 0.5, 12],
    ["L", 20, 1.5, 20, 14.5], ["L", 17.8, 1.5, 22.2, 1.5], ["L", 17.8, 14.5, 22.2, 14.5]
];
RK.I.measure = [
    ["R", 1.5, 5, 21, 7],
    ["L", 5, 5, 5, 8.5], ["L", 8.5, 5, 8.5, 7.5], ["L", 12, 5, 12, 9.5], ["L", 15.5, 5, 15.5, 7.5], ["L", 19, 5, 19, 8.5]
];
RK.I.replace = [
    ["L", 4, 4.5, 18, 4.5], ["L", 15, 1.5, 18, 4.5, 15, 7.5],
    ["L", 20, 11.5, 6, 11.5], ["L", 9, 8.5, 6, 11.5, 9, 14.5]
];
RK.I.lock = [
    ["F", 6.5, 7, 11, 8.5],
    RK.op("L", RK.arc(12, 6.5, 3.6, 180, 360, 10)),
    ["L", 8.4, 6.5, 8.4, 7.5], ["L", 15.6, 6.5, 15.6, 7.5]
];
RK.I.back = [["L", 9, 3, 3, 8, 9, 13], ["L", 16, 3, 10, 8, 16, 13]];
RK.I.ok = [["L", 3, 8.5, 8, 13, 18, 3]];
RK.I.update = [["L", 10, 15, 10, 2], ["L", 4.5, 7.5, 10, 2, 15.5, 7.5]];
RK.I.prev = [["L", 3, 2, 3, 14], ["L", 12, 2, 6, 8, 12, 14]];
RK.I.next = [["L", 17, 2, 17, 14], ["L", 8, 2, 14, 8, 8, 14]];
RK.I.minus = [["L", 4, 8, 16, 8]];
RK.I.plus = [["L", 4, 8, 16, 8], ["L", 10, 2, 10, 14]];
RK.I.info = [["C", 9, 9, 7], ["D", 9, 5.5, 1.1], ["L", 9, 8, 9, 13]];

// Draw an icon at (ox, oy) in a control's onDraw.
RK.drawIcon = function (g, ops, ox, oy, color, scale) {
    if (!ops) return;
    var k = scale || 1;
    var pen = g.newPen(g.PenType.SOLID_COLOR, color, 1.4);
    var brush = g.newBrush(g.BrushType.SOLID_COLOR, color);
    var X = function (v) { return ox + v * k; }, Y = function (v) { return oy + v * k; };
    var stroke = [], fill = [], texts = [], i, j, o;
    for (i = 0; i < ops.length; i++) {
        o = ops[i];
        if (o[0] === "D" || o[0] === "F" || o[0] === "P") fill.push(o);
        else if (o[0] === "T") texts.push(o);
        else stroke.push(o);
    }
    var trace = function (list) {
        g.newPath();
        for (var a = 0; a < list.length; a++) {
            var q = list[a];
            if (q[0] === "L" || q[0] === "Z" || q[0] === "P") {
                g.moveTo(X(q[1]), Y(q[2]));
                for (j = 3; j < q.length; j += 2) g.lineTo(X(q[j]), Y(q[j + 1]));
                if (q[0] !== "L") g.closePath();
            } else if (q[0] === "C" || q[0] === "D") {
                g.ellipsePath(X(q[1] - q[3]), Y(q[2] - q[3]), 2 * q[3] * k, 2 * q[3] * k);
            } else if (q[0] === "E") {
                g.ellipsePath(X(q[1] - q[3]), Y(q[2] - q[4]), 2 * q[3] * k, 2 * q[4] * k);
            } else if (q[0] === "R" || q[0] === "F") {
                g.rectPath(X(q[1]), Y(q[2]), q[3] * k, q[4] * k);
            }
        }
    };
    if (stroke.length) {
        trace(stroke);
        g.strokePath(pen);
    }
    if (fill.length) {
        trace(fill);
        g.fillPath(brush);
    }
    for (i = 0; i < texts.length; i++) {
        o = texts[i];
        g.drawString(o[1], pen, X(o[2]), Y(o[3]), ScriptUI.newFont("dialog", "REGULAR", (o[4] || 12) * k));
    }
};

// ============================================================================ panel

RK.ui = {
    win: null,        // the dock panel or the palette
    main: null,       // top bar, pages and status
    content: null,    // the page stack
    tabsGroup: null,
    tabDrop: null,
    tabs: [],         // {id, icon, tip, build, page, button}
    tab: null,        // id of the tab shown
    sub: null,        // the tool page shown over the tab, if any
    status: null,
    expert: false     // Settings > Expert Mode (icons only), read at start
};
RK.tools = {};        // id -> {label, icon, tip, run(), page(body, t)}

RK.ROW_H = 20;        // tool grid row height
RK.ICON_BOX = 30;     // room for a tool icon left of its label
RK.FONT = 12;
RK.PAGE_W = 300;      // width of tool pages (forms)

// Register a tool. def: {label, icon (RK.drawIcon ops), tip, run: fn} for a tool that runs at
// once, or {label, icon, tip, page: fn(body, t)} for one that opens its own page.
RK.tool = function (id, def) {
    def.id = id;
    RK.tools[id] = def;
    return def;
};

// ---------------------------------------------------------------------------- messages

RK.msg = function (s) {
    var t = String(s);
    try {
        alert(t);
    } catch (e) {}
    var st = RK.ui.status;
    if (st) {
        st.text = RK.wrap(t, 46);
        st.visible = true;
        st.__set = true;
    }
};
RK.clearMsg = function () {
    var st = RK.ui.status;
    if (st && st.__set) {
        st.text = "";
        st.visible = false;
        st.__set = false;
    }
};
RK.err = function (e) {
    return e && e.message ? e.message : String(e);
};

// ---------------------------------------------------------------------------- flat buttons

// A button EffectCraft draws from our draw list: an icon and a label, no frame. Icon and label
// turn red while the pointer is over it (and while it is "on", for toggles).
// o: {label, icon, tip, w, h, iconW, iconX, iconY, iconScale, textX, center,
//     on() (drawn red), onClick, name, iconOnly}
RK.ui.flat = function (parent, o) {
    var label = o.label || "";
    if (RK.ui.expert && !o.keepLabel && o.icon) label = "";
    var b = parent.add("button", undefined, label);
    o.shown = label;
    var w = o.w;
    if (w === undefined || w === null) w = RK.ui.flatWidth(o);
    b.preferredSize = [Math.ceil(w), o.h || RK.ROW_H];
    if (o.tip || o.label) b.helpTip = o.tip || o.label;
    if (o.name) b.name = o.name;
    b.__o = o;
    b.onDraw = RK.ui.drawFlat;
    b.addEventListener("mouseover", function () { this.__hover = true; });
    b.addEventListener("mouseout", function () { this.__hover = false; });
    b.onClick = function () {
        if (typeof this.__o.onClick === "function") this.__o.onClick.call(this);
    };
    return b;
};

// Natural width of a flat button.
RK.ui.flatWidth = function (o) {
    var label = o.shown !== undefined ? o.shown : (RK.ui.expert && !o.keepLabel && o.icon ? "" : o.label || "");
    var iw = o.icon ? (o.iconW || 24) * (o.iconScale || 1) : 0;
    if (!label) return iw + 6;
    return iw + (o.icon ? 8 : 0) + RK.textWidth(label, RK.FONT) + 10;
};

RK.ui.drawFlat = function () {
    var o = this.__o, g = this.graphics;
    var w = this.size[0], h = this.size[1];
    var on = this.enabled !== false;
    var hot = on && (this.__hover || (o.on && o.on()));
    var col = !on ? RK.C.dim : hot ? RK.C.active : RK.C.icon;
    var pen = !on ? g.newPen(g.PenType.SOLID_COLOR, RK.C.dim, 1) : hot ? g.newPen(g.PenType.SOLID_COLOR, RK.C.activeText, 1) : null;
    var k = o.iconScale || 1;
    var label = o.shown || "";
    var tw = RK.textWidth(label, RK.FONT);
    var iw = o.icon ? (o.iconW === undefined ? 24 : o.iconW) : 0;
    var ty = Math.round((h - 15) / 2);
    if (o.center || !label) {
        var gap = o.icon && label ? 8 : 0;
        var x0 = Math.round((w - (iw * k + gap + tw)) / 2);
        if (o.icon) RK.drawIcon(g, o.icon, x0, o.iconY === undefined ? Math.round((h - 16 * k) / 2) : o.iconY, col, k);
        if (label) g.drawString(label, pen, x0 + iw * k + gap, ty);
        return;
    }
    if (o.icon) {
        var ix = o.iconX === undefined ? RK.ICON_BOX - iw * k - 4 : o.iconX;
        RK.drawIcon(g, o.icon, ix, o.iconY === undefined ? Math.round((h - 16 * k) / 2) : o.iconY, col, k);
    }
    var tx = o.textX !== undefined ? o.textX : (o.icon ? RK.ICON_BOX + 4 : 4);
    g.drawString(label, pen, tx, ty);
};

// An icon check-box: red while checked. o: {label, icon, tip, value, onChange(value), w, h}
RK.ui.toggle = function (parent, o) {
    var state = { value: !!o.value };
    var b = RK.ui.flat(parent, {
        label: o.label, icon: o.icon, tip: o.tip, w: o.w, h: o.h || 22, iconW: o.iconW, iconX: o.iconX, iconY: o.iconY,
        textX: o.textX, center: o.center, name: o.name,
        on: function () { return state.value; },
        onClick: function () {
            state.value = !state.value;
            if (o.onChange) o.onChange.call(b, state.value);
        }
    });
    b.getValue = function () { return state.value; };
    b.setValue = function (v) { state.value = !!v; };
    return b;
};

// Icon radio buttons: exactly one is on. items: [{label, icon, tip}], o: {value (index),
// onChange(index), vertical, w}
RK.ui.iconRadios = function (parent, items, o) {
    var grp = parent.add("group");
    grp.orientation = o.vertical ? "column" : "row";
    grp.alignChildren = [o.vertical ? "center" : "left", "center"];
    grp.spacing = 2;
    var state = { value: o.value || 0 };
    var buttons = [];
    for (var i = 0; i < items.length; i++) {
        (function (i) {
            var it = items[i];
            buttons.push(RK.ui.flat(grp, {
                label: it.label, icon: it.icon, tip: it.tip, w: o.w, h: o.h || 22, center: o.center,
                on: function () { return state.value === i; },
                onClick: function () {
                    state.value = i;
                    if (o.onChange) o.onChange(i);
                }
            }));
        })(i);
    }
    grp.getValue = function () { return state.value; };
    grp.setValue = function (v) { state.value = v; };
    grp.buttons = buttons;
    return grp;
};

// ---------------------------------------------------------------------------- tool grids

RK.ui.runTool = function (id) {
    var t = RK.tools[id];
    if (!t) return;
    RK.clearMsg();
    if (t.page) {
        RK.ui.openPage(t);
        return;
    }
    try {
        t.run();
    } catch (e) {
        RK.msg(t.label + ": " + RK.err(e));
    }
};

RK.ui.toolButton = function (parent, id, w) {
    var t = RK.tools[id];
    if (!t) return null;
    return RK.ui.flat(parent, {
        label: t.label, icon: t.icon, tip: t.tip, w: w, name: "tool." + id,
        onClick: function () { RK.ui.runTool(id); }
    });
};

// A tab's page of tools: an optional centred top row, then two columns.
// spec: {top: [ids], left: [ids], right: [ids]}
RK.ui.buildGrid = function (page, spec) {
    page.orientation = "column";
    page.alignChildren = ["center", "top"];
    page.spacing = 2;
    var colW = function (ids) {
        if (RK.ui.expert) return RK.ICON_BOX + 4;
        var m = 0;
        for (var i = 0; i < ids.length; i++) if (ids[i] && RK.tools[ids[i]]) m = Math.max(m, RK.textWidth(RK.tools[ids[i]].label, RK.FONT));
        return RK.ICON_BOX + 4 + Math.ceil(m) + 8;
    };
    if (spec.top && spec.top.length) {
        var top = page.add("group");
        top.spacing = 6;
        for (var t = 0; t < spec.top.length; t++) {
            var tt = RK.tools[spec.top[t]];
            if (!tt) continue;
            var b = RK.ui.flat(top, {
                label: tt.label, icon: tt.icon, tip: tt.tip, center: true, name: "tool." + spec.top[t],
                onClick: (function (id) { return function () { RK.ui.runTool(id); }; })(spec.top[t])
            });
            b.alignment = ["center", "top"];
        }
    }
    var cols = page.add("group");
    cols.alignChildren = ["left", "top"];
    cols.spacing = 4;
    var sides = [spec.left || [], spec.right || []];
    for (var s = 0; s < 2; s++) {
        var ids = sides[s];
        if (!ids.length) continue;
        var col = cols.add("group");
        col.orientation = "column";
        col.alignChildren = ["left", "top"];
        col.spacing = 0;
        var w = colW(ids);
        for (var i = 0; i < ids.length; i++) {
            if (!ids[i]) {
                var gap = col.add("group");
                gap.preferredSize = [w, 8];
                continue;
            }
            RK.ui.toolButton(col, ids[i], w);
        }
    }
    return cols;
};

// ---------------------------------------------------------------------------- tabs and pages

RK.ui.addTab = function (id, label, icon, tip, build) {
    RK.ui.tabs.push({ id: id, label: label, icon: icon, tip: tip, build: build, page: null, button: null });
};

RK.ui.findTab = function (id) {
    for (var i = 0; i < RK.ui.tabs.length; i++) if (RK.ui.tabs[i].id === id) return RK.ui.tabs[i];
    return null;
};

RK.ui.newPage = function () {
    var p = RK.ui.content.add("group");
    p.orientation = "column";
    p.alignment = ["center", "top"];
    p.alignChildren = ["center", "top"];
    p.spacing = 4;
    p.margins = [0, 0, 0, 0];
    return p;
};

RK.ui.closeSub = function () {
    if (RK.ui.sub) {
        RK.ui.content.remove(RK.ui.sub);
        RK.ui.sub = null;
        RK.ui.subTool = null;
    }
};

RK.ui.showTab = function (id) {
    var tab = RK.ui.findTab(id);
    if (!tab) return;
    RK.ui.closeSub();
    for (var i = 0; i < RK.ui.tabs.length; i++) {
        var t = RK.ui.tabs[i];
        if (t.page) t.page.visible = false;
    }
    if (!tab.page) {
        tab.page = RK.ui.newPage();
        try {
            tab.build(tab.page);
        } catch (e) {
            RK.msg(tab.label + ": " + RK.err(e));
        }
    }
    tab.page.visible = true;
    RK.ui.tab = id;
    if (RK.ui.tabDrop) {
        for (var k = 0; k < RK.ui.tabs.length; k++) if (RK.ui.tabs[k].id === id) RK.ui.tabDrop.selection = k;
    }
    RK.setPref("tab", id);
    RK.ui.relayout();
};

// A tool's own page, shown over its tab until RK.ui.back(). The page function gets a body
// group RK.PAGE_W wide (alignChildren fill) and adds its controls and its bottom row
// (RK.ui.bottomRow).
RK.ui.openPage = function (t) {
    RK.ui.closeSub();
    var tab = RK.ui.findTab(RK.ui.tab);
    if (tab && tab.page) tab.page.visible = false;
    var p = RK.ui.newPage();
    p.spacing = 6;
    var title = p.add("statictext", undefined, t.label);
    var tw = RK.textWidth(t.label, RK.FONT);
    title.preferredSize = [Math.ceil((t.icon ? 32 : 0) + tw + 4), 20];
    title.onDraw = function () {
        if (t.icon) RK.drawIcon(this.graphics, t.icon, 0, 2, RK.C.icon);
        this.graphics.drawString(t.label, null, t.icon ? 32 : 0, 2);
    };
    var body = p.add("group");
    body.orientation = "column";
    body.alignChildren = ["fill", "top"];
    body.spacing = 4;
    body.preferredSize = [RK.PAGE_W, -1];
    RK.ui.sub = p;
    RK.ui.subTool = t;
    try {
        (t.pageFn || t.page)(body, t);
    } catch (e) {
        RK.msg(t.label + ": " + RK.err(e));
    }
    RK.ui.relayout();
    return body;
};

RK.ui.back = function () {
    RK.ui.showTab(RK.ui.tab);
};

RK.ui.relayout = function () {
    try {
        RK.ui.win.layout.layout(true);
    } catch (e) {}
};

// ---------------------------------------------------------------------------- form widgets

// A row: optional label (fixed width) then whatever the caller adds.
RK.ui.row = function (parent, label, labelW) {
    var r = parent.add("group");
    r.orientation = "row";
    r.alignChildren = ["left", "center"];
    r.spacing = 4;
    if (label !== undefined && label !== null && label !== "") {
        var st = r.add("statictext", undefined, label);
        st.preferredSize = [labelW || Math.ceil(RK.textWidth(label, RK.FONT)) + 4, 18];
        r.label = st;
    }
    return r;
};
RK.ui.check = function (parent, label, value, tip) {
    var c = parent.add("checkbox", undefined, label);
    c.value = !!value;
    if (tip) c.helpTip = tip;
    c.preferredSize = [Math.ceil(RK.textWidth(label, RK.FONT)) + 24, 18];
    return c;
};
RK.ui.radio = function (parent, label, value, tip) {
    var c = parent.add("radiobutton", undefined, label);
    c.value = !!value;
    if (tip) c.helpTip = tip;
    c.preferredSize = [Math.ceil(RK.textWidth(label, RK.FONT)) + 24, 18];
    return c;
};
// Radio buttons in one group: returns the group with getValue() / setValue(index).
RK.ui.radios = function (parent, labels, value, o) {
    o = o || {};
    var g = parent.add("group");
    g.orientation = o.vertical ? "column" : "row";
    g.alignChildren = ["left", "center"];
    g.spacing = o.spacing === undefined ? 8 : o.spacing;
    var rs = [];
    for (var i = 0; i < labels.length; i++) {
        (function (i) {
            var r = RK.ui.radio(g, labels[i], i === (value || 0), o.tips ? o.tips[i] : undefined);
            r.onClick = function () {
                for (var k = 0; k < rs.length; k++) rs[k].value = k === i;
                if (o.onChange) o.onChange(i);
            };
            rs.push(r);
        })(i);
    }
    g.radios = rs;
    g.getValue = function () {
        for (var k = 0; k < rs.length; k++) if (rs[k].value) return k;
        return 0;
    };
    g.setValue = function (v) {
        for (var k = 0; k < rs.length; k++) rs[k].value = k === v;
    };
    g.setEnabled = function (on) {
        for (var k = 0; k < rs.length; k++) rs[k].enabled = !!on;
    };
    return g;
};
RK.ui.number = function (parent, value, w, tip) {
    var e = parent.add("edittext", undefined, String(value));
    e.preferredSize = [w || 50, 22];
    if (tip) e.helpTip = tip;
    e.onChanging = function () {};
    return e;
};
RK.ui.text = function (parent, value, w, tip) {
    var e = parent.add("edittext", undefined, value === undefined ? "" : String(value));
    e.preferredSize = [w || 160, 22];
    if (tip) e.helpTip = tip;
    e.onChanging = function () {};
    return e;
};
RK.ui.drop = function (parent, items, sel, w, tip) {
    var d = parent.add("dropdownlist", undefined, items);
    var longest = 0;
    for (var i = 0; i < items.length; i++) longest = Math.max(longest, RK.textWidth(items[i], RK.FONT));
    d.preferredSize = [Math.max(w || 0, Math.ceil(longest) + 42), 22];
    d.selection = sel || 0;
    if (tip) d.helpTip = tip;
    return d;
};
// A plain EffectCraft button.
RK.ui.button = function (parent, label, onClick, tip, w) {
    var b = parent.add("button", undefined, label);
    b.preferredSize = [w || Math.max(60, Math.ceil(RK.textWidth(label, RK.FONT)) + 24), 24];
    if (tip) b.helpTip = tip;
    b.onClick = function () {
        RK.clearMsg();
        try {
            onClick.call(this);
        } catch (e) {
            RK.msg(label + ": " + RK.err(e));
        }
    };
    return b;
};
RK.ui.label = function (parent, text, w) {
    var s = parent.add("statictext", undefined, text);
    if (w) s.preferredSize = [w, 16 * String(text).split("\n").length];
    return s;
};
RK.ui.num = function (edit, def, min, max) {
    var v = parseFloat(String(edit.text).replace(",", "."));
    if (isNaN(v)) v = def;
    if (min !== undefined && v < min) v = min;
    if (max !== undefined && v > max) v = max;
    return v;
};
RK.ui.int = function (edit, def, min, max) {
    var v = parseInt(String(edit.text).replace(/^\s*0+(?=\d)/, ""), 10);
    if (isNaN(v)) v = def;
    if (min !== undefined && v < min) v = min;
    if (max !== undefined && v > max) v = max;
    return v;
};
RK.ui.gap = function (parent, h) {
    var g = parent.add("group");
    g.preferredSize = [4, h || 4];
    return g;
};
// A thin rule, with an optional caption on its left, RK.PAGE_W wide (or w).
RK.ui.separator = function (parent, caption, w) {
    var W = w || RK.PAGE_W;
    var s = parent.add("statictext", undefined, caption || "");
    s.preferredSize = [W, caption ? 18 : 9];
    s.alignment = ["center", "top"];
    s.onDraw = function () {
        var g = this.graphics, x = 0, y = caption ? 9.5 : 4.5;
        if (caption) {
            g.drawString(caption, g.newPen(g.PenType.SOLID_COLOR, RK.C.dim, 1), 0, 1);
            x = RK.textWidth(caption, RK.FONT) + 6;
        }
        g.newPath();
        g.moveTo(x, y);
        g.lineTo(W, y);
        g.strokePath(g.newPen(g.PenType.SOLID_COLOR, RK.C.rule, 1));
    };
    return s;
};
// The bottom row of a tool page: flat buttons with icons, centred.
// items: [{label, icon, tip, onClick}]; RK.ui.backItem() is the standard Back.
RK.ui.bottomRow = function (parent, items) {
    RK.ui.separator(parent);
    var r = parent.add("group");
    r.orientation = "row";
    r.alignment = ["center", "top"];
    r.spacing = 10;
    var out = [];
    for (var i = 0; i < items.length; i++) {
        var it = items[i];
        out.push(RK.ui.flat(r, { label: it.label, icon: it.icon, tip: it.tip, h: 24, name: it.name, center: true, onClick: it.onClick }));
    }
    r.buttons = out;
    return r;
};
RK.ui.backItem = function (label) {
    return { label: label || "Back", icon: RK.I.back, tip: label || "Back", name: "back", onClick: function () { RK.clearMsg(); RK.ui.back(); } };
};
// Runs fn; on success optionally goes back to the tab. Errors go to the status line.
RK.ui.action = function (label, fn, goBack) {
    return function () {
        RK.clearMsg();
        try {
            var r = fn.call(this);
            if (goBack && r !== false) RK.ui.back();
        } catch (e) {
            RK.msg(label + ": " + RK.err(e));
        }
    };
};

// ---------------------------------------------------------------------------- the panel

RK.ui.build = function (thisObj) {
    RK.ui.expert = !!RK.pref("expert", false);
    var docked = thisObj instanceof Panel;
    var w = docked ? thisObj : new Window("palette", RK.NAME, undefined, { resizeable: true });
    RK.ui.win = w;
    w.orientation = docked ? "stack" : "column";
    w.alignChildren = ["fill", "top"];
    w.margins = docked ? [8, 6, 8, 6] : [8, 8, 8, 8];
    w.spacing = 8;

    var main = w.add("group");
    RK.ui.main = main;
    main.orientation = "column";
    main.alignment = ["fill", "top"];
    main.alignChildren = ["fill", "top"];
    main.spacing = 10;
    if (!docked) main.preferredSize = [326, -1];
    // docked, the bottom bar shares a stack with the rest: keep its room under the content, so a
    // page taller than the panel scrolls without the bar drawn over its last row
    if (docked) main.margins = [0, 0, 0, 26];

    // top bar: the tabs, right-aligned (icons, or a dropdown)
    var bar = main.add("group");
    bar.orientation = "stack";
    bar.alignChildren = ["right", "center"];
    var useDrop = !!RK.pref("tabDropdown", false);
    var tabs = bar.add("group");
    tabs.alignment = ["right", "center"];
    tabs.spacing = 2;
    RK.ui.tabsGroup = tabs;
    for (var i = 0; i < RK.ui.tabs.length; i++) {
        (function (tab) {
            tab.button = RK.ui.flat(tabs, {
                icon: tab.icon, iconW: 20, w: 24, h: 22, iconX: 2, iconY: 2, tip: tab.tip, name: "tab." + tab.id,
                on: function () { return RK.ui.tab === tab.id; },
                onClick: function () { RK.clearMsg(); RK.ui.showTab(tab.id); }
            });
        })(RK.ui.tabs[i]);
    }
    var names = [];
    for (var j = 0; j < RK.ui.tabs.length; j++) names.push(RK.ui.tabs[j].label);
    var drop = bar.add("dropdownlist", undefined, names);
    drop.alignment = ["right", "center"];
    drop.helpTip = "Tool boxes";
    var longest = 0;
    for (var k = 0; k < names.length; k++) longest = Math.max(longest, RK.textWidth(names[k], RK.FONT));
    drop.preferredSize = [Math.ceil(longest) + 42, 22];
    drop.onChange = function () {
        if (this.selection) RK.ui.showTab(RK.ui.tabs[this.selection.index].id);
    };
    RK.ui.tabDrop = drop;
    RK.ui.setSelector(useDrop);

    var content = main.add("group");
    RK.ui.content = content;
    content.orientation = "stack";
    content.alignment = ["fill", "top"];
    content.alignChildren = ["center", "top"];

    var st = main.add("statictext", undefined, "");
    st.visible = false;
    st.alignment = ["center", "top"];
    RK.ui.status = st;

    // bottom bar: credit, logo, version. Three fixed-size pieces aligned left, centre and right
    // (a docked panel is stretched to the dock after onDraw ran, so one drawn bar can't know
    // its final width).
    var foot = w.add("group");
    foot.orientation = "stack";
    foot.alignment = ["fill", "bottom"];
    var footText = function (s, align) {
        var t = foot.add("statictext", undefined, s);
        t.preferredSize = [Math.ceil(RK.textWidth(s, RK.FONT)) + 2, 18];
        t.alignment = [align, "bottom"];
        t.onDraw = function () {
            this.graphics.drawString(s, this.graphics.newPen(this.graphics.PenType.SOLID_COLOR, RK.C.dim, 1), 0, 1);
        };
        return t;
    };
    // Expert mode is meant for a narrow panel: the logo alone
    if (!RK.ui.expert) {
        footText("for EffectCraft", "left");
        footText(RK.NAME + " " + RK.VERSION, "right");
    }
    var logo = foot.add("statictext", undefined, "");
    var logoName = RK.NAME.toUpperCase();
    logo.preferredSize = [Math.ceil(26 + RK.textWidth(logoName, 11)) + 2, 18];
    logo.alignment = ["center", "bottom"];
    logo.onDraw = function () {
        var g = this.graphics;
        RK.drawIcon(g, [["L", 2, 13, 9, 4, 18, 9], ["D", 2, 13, 1.8], ["D", 18, 9, 1.8]], 0, 0, RK.C.logoA);
        RK.drawIcon(g, [["D", 9, 4, 2.2]], 0, 0, RK.C.logoB);
        g.drawString(logoName, g.newPen(g.PenType.SOLID_COLOR, RK.C.logoA, 1), 26, 2, ScriptUI.newFont("dialog", "REGULAR", 11));
    };

    var start = RK.pref("tab", RK.ui.tabs.length ? RK.ui.tabs[0].id : null);
    if (!RK.ui.findTab(start)) start = RK.ui.tabs[0].id;
    RK.ui.showTab(start);

    if (!docked) {
        w.onResizing = w.onResize = function () { this.layout.resize(); };
        w.show();
    } else {
        w.layout.layout(true);
    }
    return w;
};

// Tab icons or the dropdown (Settings > General > Panel selector).
RK.ui.setSelector = function (useDrop) {
    if (RK.ui.tabsGroup) RK.ui.tabsGroup.visible = !useDrop;
    if (RK.ui.tabDrop) RK.ui.tabDrop.visible = !!useDrop;
};

// ============================================================================ project helpers

// Values tools share at run time (e.g. Measure fills the Wheel tool's radius).
RK.shared = {};

RK.comp = function (quiet) {
    var c = app.project.activeItem;
    if (!c || !(c instanceof CompItem)) {
        if (!quiet) RK.msg("Open a composition first.");
        return null;
    }
    return c;
};

// Run fn in one undo step; the step ends even when fn throws.
// Run fn as one undo step. When fn throws, the step is taken back (a tool that fails leaves
// nothing half-built) and the error goes on.
RK.undo = function (name, fn) {
    app.beginUndoGroup(name);
    var done = false;
    try {
        var r = fn();
        done = true;
        return r;
    } finally {
        app.endUndoGroup();
        if (!done) RK.undoLast(name);
    }
};

// Take back the newest undo step when it is the one named.
RK.undoLast = function (name) {
    try {
        var h = app.run("edit.history", {});
        if (h && h.undo && h.undo.length && h.undo[0] === name) app.run("edit.history", { steps: 1 });
    } catch (e) {}
};

RK.selLayers = function (comp) {
    return comp.selectedLayers;
};

// Selected properties that hold values (not groups), in selection order, without duplicates.
RK.selProps = function (comp) {
    var all = comp.selectedProperties, out = [], seen = {};
    for (var i = 0; i < all.length; i++) {
        var p = all[i];
        if (!(p instanceof Property)) continue;
        var k = p.__layer.__id + ":" + p.__uid;
        if (seen[k]) continue;
        seen[k] = true;
        out.push(p);
    }
    return out;
};

RK.layerOf = function (prop) {
    return prop.__layer;
};

RK.layerNames = function (comp) {
    var names = {}, n = comp.numLayers;
    try {
        var info = app.run("comp.info", { comp: comp.id }).layers;
        for (var i = 0; i < info.length; i++) names[info[i].name] = true;
        return names;
    } catch (e) {}
    for (var j = 1; j <= n; j++) names[comp.layer(j).name] = true;
    return names;
};

// A layer name not used in the comp yet: base, then "base 2", "base 3"...
RK.uniqueName = function (comp, base, names) {
    names = names || RK.layerNames(comp);
    var n = base, i = 2;
    while (names[n]) n = base + " " + i++;
    names[n] = true;
    return n;
};

RK.is3D = function (layer) {
    try {
        return !!layer.threeDLayer;
    } catch (e) {
        return false;
    }
};

RK.isCamera = function (layer) {
    return layer instanceof CameraLayer;
};

// ---------------------------------------------------------------------------- effects

RK.effects = function (layer) {
    return layer.property("ADBE Effect Parade");
};

RK.addEffect = function (layer, matchName, name) {
    var e = RK.effects(layer).addProperty(matchName);
    if (name) e.name = name;
    return e;
};

RK.findEffect = function (layer, name) {
    var fx = RK.effects(layer);
    if (!fx) return null;
    for (var i = 1; i <= fx.numProperties; i++) if (fx.property(i).name === name) return fx.property(i);
    return null;
};

RK.addSlider = function (layer, name, v) {
    var e = RK.addEffect(layer, "ADBE Slider Control", name);
    if (v !== undefined) e.property(1).setValue(v);
    return e;
};
RK.addAngle = function (layer, name, v) {
    var e = RK.addEffect(layer, "ADBE Angle Control", name);
    if (v !== undefined) e.property(1).setValue(v);
    return e;
};
RK.addCheckbox = function (layer, name, v) {
    var e = RK.addEffect(layer, "ADBE Checkbox Control", name);
    if (v !== undefined) e.property(1).setValue(v ? 1 : 0);
    return e;
};
RK.addPoint = function (layer, name, v) {
    var e = RK.addEffect(layer, "ADBE Point Control", name);
    if (v !== undefined) e.property(1).setValue(v);
    return e;
};
RK.add3DPoint = function (layer, name, v) {
    var e = RK.addEffect(layer, "ADBE Point3D Control", name);
    if (v !== undefined) e.property(1).setValue(v);
    return e;
};
RK.addColor = function (layer, name, v) {
    var e = RK.addEffect(layer, "ADBE Color Control", name);
    if (v !== undefined) e.property(1).setValue(v);
    return e;
};
RK.addLayerControl = function (layer, name, target) {
    var e = RK.addEffect(layer, "ADBE Layer Control", name);
    if (target) e.property(1).setValue(target.index);
    return e;
};
RK.addDropdown = function (layer, name, items, sel) {
    var e = RK.addEffect(layer, "ADBE Dropdown Control", name);
    var menu = e.property(1);
    app.run("effect.editDropdown", { comp: layer.containingComp.id, layer: layer.id, prop: menu.__uid, items: items });
    if (sel) menu.setValue(sel);
    return e;
};

// ---------------------------------------------------------------------------- expressions
//
// Every expression Rig Kit writes starts with a comment line naming the tool, so tools can
// find, replace or remove their own expressions and leave others alone.
//
// EffectCraft runs an expression again from the top each time it needs a value it hasn't got
// yet. The expressions read every value through rk() first: rk() lets a run go on when a value
// isn't there yet, so one run asks for all the values that don't depend on each other, and
// the result is only computed once nothing is missing (rk_wait false). The guard is harmless
// elsewhere: errors that aren't "value not there yet" go through.

RK.TAG = "// " + RK.NAME + ": ";

RK.xHead = function (tool) {
    return RK.TAG + tool + "\n" +
        "var rk_wait = false;\n" +
        "function rk(f) { try { return f(); } catch (e) { if (String(e && e.message).charCodeAt(0) === 1) { rk_wait = true; return null; } throw e; } }\n";
};

// Fast reads for expressions that read many values (rigs). EffectCraft's expression engine
// answers each value through one host request; going through the After Effects object model
// (thisComp.layer("A").effect("B")(1).value) costs several requests and proxy objects per value.
// These helpers ask the host directly when it is there (about 7x quicker for rigs), and fall
// back to the object model otherwise. A layer is an id on the fast path, a Layer object
// otherwise; values go through rk() like any other read.
RK.xFast =
    "var rkF = typeof __h === \"function\" && typeof __VALUE === \"number\" && typeof __m4 === \"function\" && thisComp.__c !== undefined;\n" +
    "function rkLayer(n) { if (!rkF) return thisComp.layer(n); var id = __h(__LAYER, thisComp.__c, n); if (id === null) throw new Error(\"Layer \\\"\" + n + \"\\\" does not exist\"); return id; }\n" +
    "function rkVal(L, path, api) { return rkF && path ? __h(__VALUE, thisComp.__c, L, path, time, false) : api(L); }\n" +
    "function rkXf(L) { return rkF ? __h(__XFORM, thisComp.__c, L, time) : L; }\n" +
    "function rkW(X, p) { return rkF ? __m4(X[2], p, false) : X.toWorld(p); }\n" +
    "function rkWv(X, v) { return rkF ? __m4(X[2], v, true) : X.toWorldVec(v); }\n" +
    "function rkParent(L) { if (!rkF) return L.hasParent ? L.parent : false; var p = __h(__LINFO, thisComp.__c, L)[7]; return p === null ? false : p; }\n";

// Host path of an effect's first parameter, or "" when the name can't be a path segment
// (paths split at "/" and "."): the read then goes through the object model.
RK.fxPath = function (effectName) {
    return /[\/.]/.test(effectName) ? "" : "effects/" + effectName + "/#1";
};

RK.q = function (s) {
    return "\"" + String(s).replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\"";
};

// Lines that look a layer up into a variable: `var v = rk(function () { return thisComp.layer("name"); });`
RK.xLayer = function (v, layerName) {
    return "var " + v + " = rk(function () { return thisComp.layer(" + RK.q(layerName) + "); });\n";
};

// A value read through rk(): `var v = obj && rk(function () { return code; });`
RK.xGet = function (v, guard, code) {
    return "var " + v + " = " + (guard ? guard + " && " : "") + "rk(function () { return " + code + "; });\n";
};

RK.setExpr = function (prop, code) {
    prop.expression = code;
    var err = "";
    try {
        err = prop.expressionError;
    } catch (e) {}
    if (err) throw new Error("expression on " + prop.name + ": " + err);
    return prop;
};

RK.isOurExpr = function (prop, tool) {
    var e = "";
    try {
        e = prop.expression;
    } catch (err) {
        return false;
    }
    return e.indexOf(RK.TAG + (tool || "")) === 0;
};

// ---------------------------------------------------------------------------- positions

// Where a layer's point (layer space, default its anchor point) is in the comp, now.
RK.compPoint = function (layer, pt) {
    var p = pt || layer.transform.anchorPoint.value;
    if (layer instanceof AVLayer && !RK.is3D(layer)) {
        try {
            var r = layer.sourcePointToComp([p[0], p[1]]);
            if (r) return [r[0], r[1]];
        } catch (e) {}
    }
    return RK.evalPoint(layer, "toComp(" + JSON.stringify([p[0], p[1], p.length > 2 ? p[2] : 0]) + ")");
};

// Evaluate a point-valued expression in the context of `layer` (thisLayer), through a
// temporary Point Control on a temporary null. Used for what scripts can't ask directly.
RK.evalPoint = function (layer, code) {
    var comp = layer.containingComp;
    var tmp = comp.layers.addNull();
    try {
        var fx = RK.addEffect(tmp, "ADBE Point3D Control", "rk tmp");
        fx.property(1).expression = "var L = thisComp.layer(" + layer.index + ");\nvar r = L." + code + ";\n[r[0], r[1], r.length > 2 ? r[2] : 0];";
        var v = fx.property(1).valueAtTime(comp.time, false);
        return [v[0], v[1], v[2]];
    } finally {
        tmp.remove();
    }
};

// ---------------------------------------------------------------------------- layer creation

RK.newNull = function (comp, name, pos) {
    var n = comp.layers.addNull();
    n.name = name;
    if (pos) n.transform.position.setValue([pos[0], pos[1], pos.length > 2 ? pos[2] : 0]);
    return n;
};

// The layer sits right above `ref` in the layer stack.
RK.placeAbove = function (layer, ref) {
    try {
        if (ref) layer.moveBefore(ref);
    } catch (e) {}
};

// ============================================================================ controller shapes
//
// Controllers are shape layers drawn as guide layers: visible and clickable in the viewer (a
// null isn't drawn there), never rendered. Each shape is one or more paths in a unit box
// (-1..1) scaled by the controller size.

RK.shapeLib = {};

// circle approximated by four cubic segments (kappa)
RK.K = 0.5523;
RK.circlePath = function (r, cx, cy) {
    cx = cx || 0;
    cy = cy || 0;
    var k = RK.K * r;
    return {
        v: [[cx, cy - r], [cx + r, cy], [cx, cy + r], [cx - r, cy]],
        i: [[-k, 0], [0, -k], [k, 0], [0, k]],
        o: [[k, 0], [0, k], [-k, 0], [0, -k]],
        closed: true
    };
};
RK.polyPath = function (pts, closed) {
    var z = [];
    for (var i = 0; i < pts.length; i++) z.push([0, 0]);
    return { v: pts, i: z, o: z.slice(), closed: closed !== false };
};
// an arrow head pointing along (dx, dy) with its tip at (x, y)
RK.arrowHead = function (x, y, dx, dy, s) {
    var l = Math.sqrt(dx * dx + dy * dy) || 1;
    dx /= l;
    dy /= l;
    var px = -dy, py = dx;
    return RK.polyPath([[x, y], [x - dx * s + px * s * 0.6, y - dy * s + py * s * 0.6], [x - dx * s - px * s * 0.6, y - dy * s - py * s * 0.6]], true);
};

RK.shapeLib.circle = function () { return [RK.circlePath(1)]; };
RK.shapeLib.square = function () { return [RK.polyPath([[-1, -1], [1, -1], [1, 1], [-1, 1]])]; };
RK.shapeLib.diamond = function () { return [RK.polyPath([[0, -1], [1, 0], [0, 1], [-1, 0]])]; };
RK.shapeLib.cross = function () {
    return [RK.polyPath([[-1, 0], [1, 0]], false), RK.polyPath([[0, -1], [0, 1]], false), RK.circlePath(0.35)];
};
// four arrows: move in any direction
RK.shapeLib.move = function () {
    var out = [RK.circlePath(0.45), RK.polyPath([[-0.95, 0], [0.95, 0]], false), RK.polyPath([[0, -0.95], [0, 0.95]], false)];
    out.push(RK.arrowHead(1.1, 0, 1, 0, 0.3), RK.arrowHead(-1.1, 0, -1, 0, 0.3), RK.arrowHead(0, 1.1, 0, 1, 0.3), RK.arrowHead(0, -1.1, 0, -1, 0.3));
    return out;
};
RK.shapeLib.horizontal = function () {
    return [RK.polyPath([[-1, 0], [1, 0]], false), RK.arrowHead(1.15, 0, 1, 0, 0.35), RK.arrowHead(-1.15, 0, -1, 0, 0.35), RK.circlePath(0.3)];
};
RK.shapeLib.vertical = function () {
    return [RK.polyPath([[0, -1], [0, 1]], false), RK.arrowHead(0, 1.15, 0, 1, 0.35), RK.arrowHead(0, -1.15, 0, -1, 0.35), RK.circlePath(0.3)];
};
// a circular arrow: rotation
RK.shapeLib.rotation = function () {
    var pts = [], n = 10, a0 = -200, a1 = 20;
    for (var i = 0; i <= n; i++) {
        var a = (a0 + (a1 - a0) * i / n) * Math.PI / 180;
        pts.push([Math.cos(a), Math.sin(a)]);
    }
    var last = (a1) * Math.PI / 180;
    return [RK.polyPath(pts, false), RK.arrowHead(Math.cos(last) + 0.05, Math.sin(last) + 0.25, -Math.sin(last), Math.cos(last), 0.35), RK.circlePath(0.18)];
};
// an eye: lids and iris
RK.shapeLib.eye = function () {
    var k = 0.55;
    return [
        { v: [[-1, 0], [1, 0]], i: [[0, 0], [-k, -0.75]], o: [[k, -0.75], [0, 0]], closed: false },
        { v: [[-1, 0], [1, 0]], i: [[0, 0], [-k, 0.75]], o: [[k, 0.75], [0, 0]], closed: false },
        RK.circlePath(0.35)
    ];
};
// a crosshair box for cameras
RK.shapeLib.camera = function () {
    return [RK.polyPath([[-1, -0.6], [0.5, -0.6], [0.5, 0.6], [-1, 0.6]]), RK.polyPath([[0.5, -0.25], [1, -0.55], [1, 0.55], [0.5, 0.25]])];
};
// a slider track with its knob at 0 (the knob is the layer itself: see the Slider controls)
RK.shapeLib.slider = function () {
    return [RK.polyPath([[-1, 0], [1, 0]], false), RK.polyPath([[-1, -0.25], [-1, 0.25]], false), RK.polyPath([[1, -0.25], [1, 0.25]], false)];
};
RK.shapeLib.knob = function () { return [RK.circlePath(0.3)]; };
// a 2D box
RK.shapeLib.box = function () { return [RK.polyPath([[-1, -1], [1, -1], [1, 1], [-1, 1]])]; };
// a bone: a long diamond along +x, from 0 to 1
RK.shapeLib.bone = function () {
    return [RK.polyPath([[0, 0], [0.2, -0.12], [1, 0], [0.2, 0.12]]), RK.circlePath(0.08)];
};
// a foot/hand: a rounded triangle pointing right
RK.shapeLib.foot = function () {
    return [RK.polyPath([[-0.8, -0.6], [1, 0], [-0.8, 0.6]]), RK.circlePath(0.25, -0.35, 0)];
};

// Shape layer content: each path in its own group (so open and closed paths stroke right),
// all stroked with one colour and width; optional fill.
RK.drawShapeLayer = function (layer, paths, size, color, width, fill) {
    var root = layer.property("ADBE Root Vectors Group");
    var grp = root.addProperty("ADBE Vector Group");
    grp.name = "Icon";
    var items = grp.property("ADBE Vectors Group");
    for (var i = 0; i < paths.length; i++) {
        var p = paths[i], sh = new Shape();
        var sc = function (a) {
            var o = [];
            for (var j = 0; j < a.length; j++) o.push([a[j][0] * size, a[j][1] * size]);
            return o;
        };
        sh.vertices = sc(p.v);
        sh.inTangents = sc(p.i);
        sh.outTangents = sc(p.o);
        sh.closed = p.closed !== false;
        var pp = items.addProperty("ADBE Vector Shape - Group");
        pp.property("ADBE Vector Shape").setValue(sh);
    }
    var st = items.addProperty("ADBE Vector Graphic - Stroke");
    st.property("ADBE Vector Stroke Color").setValue([color[0], color[1], color[2]]);
    st.property("ADBE Vector Stroke Width").setValue(width || 3);
    if (fill) {
        var fl = items.addProperty("ADBE Vector Graphic - Fill");
        fl.property("ADBE Vector Fill Color").setValue([color[0], color[1], color[2]]);
        fl.property("ADBE Vector Fill Opacity").setValue(fill);
    }
    return grp;
};

// ============================================================================ Rig expressions (RKX)
//
// EffectCraft evaluates an expression each time something reads its property, and nothing is
// cached between evaluations: reading a layer whose transform (or a parent's) is driven by
// expressions evaluates them again, and those read further layers in turn, so a rig whose
// expressions read each other gets slower with every level (a whole character took seconds a
// frame). Every + - * / of an expression is also a function call (After Effects' array maths).
//
// So every rig expression Rig Kit writes is the same small program:
//  * The maths live in one library (RK.rkx.factory), compiled once per expression thread with
//    new Function (plain JavaScript operators) and kept in a global for later evaluations.
//  * Each expression carries a spec (JSON): the layers it needs and the formula of every
//    rig-driven transform between them and what the animator keys. It computes those transforms
//    itself instead of reading them, so an evaluation only reads keyed values, effect parameters
//    and the transforms of layers no rig expression drives.
//  * Reads go to the engine's host requests by layer id: the first run of an evaluation asks
//    for all of them, the second reads them and does the maths. Ids survive renaming; names are
//    the fallback in a duplicated comp. A node whose parent changed since the rig was built is
//    read from the engine instead (slower, still right).
//  * The renderer evaluates a layer's position about three times a frame and its rotation
//    twice. A layer's position and rotation expressions share one compiled function (a family,
//    found by a short key in the text) that computes both, and it keeps its last inputs and
//    outputs: when every value read is the same as last time, the maths are skipped. The cache
//    is exact (all inputs compared), so an edit is seen at once.
//  * Auto-Rig limbs are flattened: their layers sit under the limb's frame and their expressions
//    show where the chain puts them, so drawing a layer doesn't evaluate a chain of parents.
//
// Script side: tools register formulas on a builder (RK.rkx.with); the builder writes every
// expression at the end, upstream first, each with its no-jump offset, then rewrites the other
// rig expressions of the comp that inlined a layer it changed.

RK.rkx = {};

// The library. Written as a function so the script can read its source (toString); it runs only
// inside expressions. Keep it free of "//" and "/*" inside strings, and of regular expressions
// (RK.rkx.minify strips comments with a simple scan).
RK.rkx.factory = function rkxLib() {
    var H = typeof __h === "function" ? __h : null;
    var QV = typeof __VALUE === "number" ? __VALUE : 6;
    var QX = typeof __XFORM === "number" ? __XFORM : 7;
    var QL = typeof __LAYER === "number" ? __LAYER : 2;
    var QI = typeof __LINFO === "number" ? __LINFO : 3;
    var QC = typeof __CHILD === "number" ? __CHILD : 4;
    var DEG = Math.PI / 180, ID = [1, 0, 0, 1, 0, 0];
    var PATH = { p: "transform/position", r: "transform/rotation", s: "transform/scale", a: "transform/anchor" };
    var fams = {}, count = 0;

    function ang(x, y) { return Math.atan2(y, x) / DEG; }
    function cl(x) { return x < -1 ? -1 : x > 1 ? 1 : x; }
    function wrap(r, lo) { return lo + ((r - lo) % 360 + 360) % 360; }
    function num(r) { return typeof r === "number" ? r : r === true ? 1 : r && r.length ? r[0] : 0; }
    function vec(r) { return r && r.length ? r : [0, 0, 0]; }
    function z3(p) { return p.length > 2 ? p[2] : 0; }
    /* 2D affine [a, b, c, d, tx, ty]: x' = a x + b y + tx, y' = c x + d y + ty */
    function mul(A, B) {
        return [A[0] * B[0] + A[1] * B[2], A[0] * B[1] + A[1] * B[3], A[2] * B[0] + A[3] * B[2], A[2] * B[1] + A[3] * B[3],
            A[0] * B[4] + A[1] * B[5] + A[4], A[2] * B[4] + A[3] * B[5] + A[5]];
    }
    function ap(A, p) { return [A[0] * p[0] + A[1] * p[1] + A[4], A[2] * p[0] + A[3] * p[1] + A[5]]; }
    function det(A) { return A[0] * A[3] - A[1] * A[2]; }
    function inv(A) {
        var d = det(A);
        if (!d) return ID;
        var a = A[3] / d, b = -A[1] / d, c = -A[2] / d, e = A[0] / d;
        return [a, b, c, e, -(a * A[4] + b * A[5]), -(c * A[4] + e * A[5])];
    }
    /* position, rotation, scale, anchor -> parent space */
    function mat(p, r, s, an) {
        var t = r * DEG, co = Math.cos(t), si = Math.sin(t), sx = s[0] / 100, sy = s[1] / 100;
        var a = co * sx, b = -si * sy, c = si * sx, d = co * sy;
        return [a, b, c, d, p[0] - a * an[0] - b * an[1], p[1] - c * an[0] - d * an[1]];
    }
    /* mul(W, mat(p, r, s, an)) in one go */
    function mm(W, p, r, s, an) {
        var t = r * DEG, co = Math.cos(t), si = Math.sin(t), sx = s[0] / 100, sy = s[1] / 100;
        var a = co * sx, b = -si * sy, c = si * sx, d = co * sy, x = p[0] - a * an[0] - b * an[1], y = p[1] - c * an[0] - d * an[1];
        var A0 = W[0], A1 = W[1], A2 = W[2], A3 = W[3];
        return [A0 * a + A1 * c, A0 * b + A1 * d, A2 * a + A3 * c, A2 * b + A3 * d, A0 * x + A1 * y + W[4], A2 * x + A3 * y + W[5]];
    }
    function m4(m) { return m && m.length >= 8 ? [m[0], m[1], m[4], m[5], m[3], m[7]] : ID; }
    function m4x(r) { return r ? m4(r[2]) : ID; }

    /* ---- the formulas, from values ---- */
    /* One-layer IK: the layer at b (in the comp) points at c; W its parent's world matrix. */
    function look(W, b, c, rev, w, fk, pre) {
        var th = rev ? ang(b[0] - c[0], b[1] - c[1]) : ang(c[0] - b[0], c[1] - b[1]);
        return pre + fk + (det(W) < 0 ? -1 : 1) * (w / 100 * th - ang(W[0], W[2]));
    }
    /* IK Goal: keeps its orientation in the comp (plus the controller's, Wc), whatever its parents do. */
    function goal(W, on, Wc, cr, pre) {
        var m = det(W) < 0 ? -1 : 1, rho = ang(W[0], W[2]);
        if (!Wc) return on ? pre - m * rho : pre;
        return on ? pre + m * (ang(Wc[0], Wc[2]) - rho) : pre + cr;
    }
    /* Two- and three-layer IK. W: the root's parent's world matrix; p0: the root's position;
       sc: the bones' scales; cw: the controller in the comp; g: Clockwise, Disable IK, Upper FK,
       (Middle FK), Lower FK, Auto-Stretch, Stretch, Auto-Shrink; lim: IK Limit; A, E: each bone's
       anchor and end in its own layer. Returns the local rotations r and stretch factors f. */
    function solve(W, p0, sc, cw, g, lim, A, E) {
        var n = A.length, i, pw = ap(W, p0);
        var dt = det(W), m0 = dt < 0 ? -1 : 1, k = Math.sqrt(Math.abs(dt)) || 1, rho0 = ang(W[0], W[2]), j = n === 3 ? 4 : 3;
        var pCw = g[0], pFk = g[1], pUp = g[2], pMid = n === 3 ? g[3] : 0, pLo = g[j], pAuto = g[j + 1], pSt = g[j + 2], pSh = g[j + 3];
        /* per bone: scale, vector from anchor to end (scaled), length in the comp */
        var Sx = [0, 0, 0], Sy = [0, 0, 0], ux = [0, 0, 0], uy = [0, 0, 0], L = [0, 0, 0], ff = [1, 1, 1], th = [0, 0, 0], Ls = 0;
        for (i = 0; i < n; i++) {
            var sx = sc[i][0] / 100, sy = sc[i][1] / 100, Ai = A[i], Ei = E[i];
            if (i > 0) k *= Math.sqrt(Math.abs(Sx[i - 1] * Sy[i - 1]));
            Sx[i] = sx;
            Sy[i] = sy;
            ux[i] = (Ei[0] - Ai[0]) * sx;
            uy[i] = (Ei[1] - Ai[1]) * sy;
            L[i] = k * Math.sqrt(ux[i] * ux[i] + uy[i] * uy[i]);
            Ls += L[i];
        }
        var Lt = Ls || 1, ax = cw[0] - pw[0], ay = cw[1] - pw[1], d = Math.sqrt(ax * ax + ay * ay);
        var ik = !pFk, Lp = Lt + pSt, e = 0;
        if (ik && pSh > 0 && d < Lp) {
            var sh = 1 - d / Lp;
            e = Lt * pSh / 100 * (1 - (1 - sh) * (1 - sh) * (1 + sh));
        }
        /* each bone stretches (or shrinks) in proportion to its length */
        var grow = ik && pAuto && d > Lp ? d - Lt : pSt;
        for (i = 0; i < n; i++) ff[i] = L[i] > 0 ? (L[i] + (grow - e) * L[i] / Lt) / L[i] : 1;
        var sw = (pCw ? 1 : -1) * m0, t0 = ang(ax, ay), bb = Math.max(d, 1e-6);
        if (n === 2) {
            var c2 = L[0] * ff[0], a2 = L[1] * ff[1], xx = (bb * bb + c2 * c2 - a2 * a2) / (2 * bb);
            var al = c2 > 0 ? Math.acos(cl(xx / c2)) / DEG : 0, ga = a2 > 0 ? Math.acos(cl((bb - xx) / a2)) / DEG : 0;
            th[0] = t0 - sw * al;
            th[1] = t0 + sw * ga;
        } else {
            /* the U solution: root and end bones make the same angle with the line to the controller */
            var U = L[0] * ff[0], M = L[1] * ff[1], Lw = L[2] * ff[2], K = bb;
            var disc = Math.max(0, K * K * (U + Lw) * (U + Lw) - 4 * U * Lw * (K * K + (U - Lw) * (U - Lw) - M * M));
            var cq = U * Lw > 0 ? cl((K * (U + Lw) - Math.sqrt(disc)) / (4 * U * Lw)) : 1, qq = Math.acos(cq) / DEG;
            var d5 = Math.sqrt(Math.max(0, K * K + Lw * Lw - 2 * K * Lw * cq)), d6 = Math.sqrt(Math.max(0, K * K + U * U - 2 * K * U * cq));
            var fM = U > 0 && M > 0 ? Math.acos(cl((U * U + M * M - d5 * d5) / (2 * U * M))) / DEG : 180;
            var fL = Lw > 0 && M > 0 ? Math.acos(cl((Lw * Lw + M * M - d6 * d6) / (2 * Lw * M))) / DEG : 180;
            th[0] = t0 - sw * qq;
            th[1] = th[0] + sw * (180 - fM);
            th[2] = th[1] + sw * (180 - fL);
        }
        /* local rotations, bone by bone: rho, m the parent's world angle and mirror */
        var r = n === 3 ? [0, 0, 0] : [0, 0], rho = rho0, m = m0, au0 = 0;
        for (i = 0; i < n; i++) {
            var au = ang(ux[i], uy[i]), ri;
            if (ik) ri = wrap(m * (th[i] - rho) - au, i === 0 ? lim : au0 - au - 180);
            else ri = i === 0 ? pUp : n === 3 && i === 1 ? pMid : pLo;
            r[i] = ri;
            rho += m * (ri + (Sx[i] < 0 ? 180 : 0));
            if (Sx[i] * Sy[i] < 0) m = -m;
            au0 = au;
        }
        return { r: r, f: ff };
    }
    /* An IK output: r0..r2 rotations; p1, p2 the next bone's (stretched) position; gp the goal's
       position (the end of the last bone); ep the IK End point (plus the effect offset e). */
    function ikOut(R, o, A, E, e, pre) {
        if (o.charAt(0) === "r") return R.r[+o.charAt(1)];
        var i = o === "gp" || o === "ep" ? A.length - 1 : +o.charAt(1) - 1, s = R.f[i];
        var x = A[i][0] + (E[i][0] - A[i][0]) * s, y = A[i][1] + (E[i][1] - A[i][1]) * s;
        if (o === "ep") return [x + e[0], y + e[1]];
        return [x, y, z3(vec(pre))];
    }
    /* Bezier IK: the curve from the end controller e to the root controller r. */
    function curve(e, r, k1, k2, off, auto) { return { e: e, r: r, k1: k1, k2: k2, off: off, auto: auto }; }
    function bzPos(Z, t0, own, p0, pre) {
        var t = t0 + Z.off / 100 + own / 100, u = 1 - t, qx, qy;
        if (Z.k2) {
            var a = u * u * u, b = 3 * t * u * u, c = 3 * t * t * u, d = t * t * t;
            qx = a * Z.e[0] + b * Z.k1[0] + c * Z.k2[0] + d * Z.r[0];
            qy = a * Z.e[1] + b * Z.k1[1] + c * Z.k2[1] + d * Z.r[1];
        } else {
            qx = u * u * Z.e[0] + 2 * t * u * Z.k1[0] + t * t * Z.r[0];
            qy = u * u * Z.e[1] + 2 * t * u * Z.k1[1] + t * t * Z.r[1];
        }
        var p = vec(pre);
        return [p[0] + qx - p0[0], p[1] + qy - p0[1], z3(p)];
    }
    function bzRot(Z, me, pv, pre) { return Z.auto ? pre + ang(me[0] - pv[0], me[1] - pv[1]) : pre; }
    /* Foot roll pivots (Auto-Rig). */
    function roll(m, tip, heel, rl, pre) {
        if (m === "tip") return pre + tip;
        if (m === "nwd") return pre + rl - tip;
        if (m === "er") return pre + (rl < 0 ? rl + heel : heel);
        if (m === "el") return pre + (rl > 0 ? rl + heel : heel);
        if (m === "nwr") return pre + (rl > 0 ? rl : 0);
        if (m === "nwl") return pre + (rl < 0 ? rl : 0);
        return pre;
    }
    /* The shoulder follows a share w of the controller c, seen from its parent W. */
    function apos(W, c, w, l0, pre) {
        var l = ap(inv(W), c), p = vec(pre);
        return [p[0] + w / 100 * (l[0] - l0[0]), p[1] + w / 100 * (l[1] - l0[1]), z3(p)];
    }
    /* A flattened layer (Auto-Rig limbs): the rig computes its transform through its virtual
       parents (W, world); it shows it from its actual parent (Wa). k: "p" or "r"; a: its anchor
       point, s: its scale. */
    function flat(W, Wa, a, s, k, pre) {
        var M = mul(inv(Wa), W);
        if (k === "p") {
            var p = ap(M, a);
            return [p[0], p[1], z3(vec(pre))];
        }
        return ang(M[0], M[2]) - (s[0] < 0 ? 180 : 0);
    }
    /* ---- 3D chains (walker): a chain of 3D layers turns about Z in the plane of its root's frame
       (its parent, its position, its Orientation), so the formulas work in that plane. 4x4
       matrices row-major, as the engine gives them. ---- */
    var I4 = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
    function m4m(A, B) {
        var o = [], i, j;
        for (i = 0; i < 4; i++) {
            for (j = 0; j < 4; j++) o.push(A[i * 4] * B[j] + A[i * 4 + 1] * B[4 + j] + A[i * 4 + 2] * B[8 + j] + A[i * 4 + 3] * B[12 + j]);
        }
        return o;
    }
    function m4p(M, p) {
        var x = p[0], y = p[1], z = p.length > 2 ? p[2] : 0;
        return [M[0] * x + M[1] * y + M[2] * z + M[3], M[4] * x + M[5] * y + M[6] * z + M[7], M[8] * x + M[9] * y + M[10] * z + M[11]];
    }
    /* Orientation (degrees): Z * Y * X */
    function rot4(o) {
        var cx = Math.cos(o[0] * DEG), sx = Math.sin(o[0] * DEG), cy = Math.cos(o[1] * DEG), sy = Math.sin(o[1] * DEG);
        var cz = Math.cos(o[2] * DEG), sz = Math.sin(o[2] * DEG);
        var X = [1, 0, 0, 0, 0, cx, -sx, 0, 0, sx, cx, 0, 0, 0, 0, 1], Y = [cy, 0, sy, 0, 0, 1, 0, 0, -sy, 0, cy, 0, 0, 0, 0, 1];
        var Z = [cz, -sz, 0, 0, sz, cz, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
        return m4m(Z, m4m(Y, X));
    }
    function tr4(M) { return [M[0], M[4], M[8], M[12], M[1], M[5], M[9], M[13], M[2], M[6], M[10], M[14], M[3], M[7], M[11], M[15]]; }
    function nrm3(v) {
        var l = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) || 1;
        return [v[0] / l, v[1] / l, v[2] / l];
    }
    function dot3(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
    /* How far a layer (world 4x4 C) is turned in the plane of the frame F, about its normal (the
       twist of its rotation seen from F; for a front plane, its Z rotation). */
    function turn3(F, C) {
        var x = nrm3([F[0], F[4], F[8]]), y0 = [F[1], F[5], F[9]];
        var z = nrm3([x[1] * y0[2] - x[2] * y0[1], x[2] * y0[0] - x[0] * y0[2], x[0] * y0[1] - x[1] * y0[0]]);
        var y = [z[1] * x[2] - z[2] * x[1], z[2] * x[0] - z[0] * x[2], z[0] * x[1] - z[1] * x[0]];
        var cx = nrm3([C[0], C[4], C[8]]), cy = nrm3([C[1], C[5], C[9]]);
        return Math.atan2(dot3(y, cx) - dot3(x, cy), dot3(x, cx) + dot3(y, cy)) / DEG;
    }
    function mv4(p) { return [1, 0, 0, p[0], 0, 1, 0, p[1], 0, 0, 1, p.length > 2 ? p[2] : 0, 0, 0, 0, 1]; }
    /* A controller's lock (its "// Rig Kit: lock" expression): the constants it shows instead of
       the static value under it, per value (null: that value isn't locked). */
    function lk(pre, v) {
        if (typeof v === "number") return v;
        var p = vec(pre), o = [], i;
        for (i = 0; i < p.length; i++) o.push(i < v.length && v[i] !== null ? v[i] : p[i]);
        return o;
    }
    /* Neck: a share of the shoulders controller's rotation rs, less a share of the torso's turn. */
    function neck(T, t0, rs, n, on, pre) {
        var d = T - t0;
        d = ((d + 180) % 360 + 360) % 360 - 180;
        return pre + rs / n - (on ? d / n : 0);
    }
    var LIB = { ang: ang, num: num, vec: vec, mul: mul, ap: ap, mat: mat, mm: mm, m4x: m4x, ID: ID, look: look, goal: goal, solve: solve,
        ikOut: ikOut, curve: curve, bzPos: bzPos, bzRot: bzRot, roll: roll, apos: apos, neck: neck, flat: flat, lk: lk };
    var NAMES = [];
    for (var nm in LIB) NAMES.push(nm + " = L." + nm);

    /* Two lists of inputs the same, value by value (numbers, or arrays of numbers)? */
    function same(A, B) {
        var n = A.length, i, j;
        if (B.length !== n) return false;
        for (i = 0; i < n; i++) {
            var a = A[i], b = B[i];
            if (a === b) continue;
            if (!a || !b || typeof a !== "object" || typeof b !== "object" || a.length !== b.length) return false;
            for (j = 0; j < a.length; j++) if (a[j] !== b[j]) return false;
        }
        return true;
    }
    LIB.same = same;
    NAMES.push("same = L.same");

    /* A spec's family: the outputs computed together. The position and rotation of a layer come
       from the same chain, and EffectCraft evaluates them one after the other (and each a few
       times a frame), so their expressions share one compiled function and one cache. */
    function family(S) {
        var o = S.o, n = S.N[o.l], ks = [], k;
        if (!n) return null;
        if (o.f && o.f.k === "flat") return { ks: ["p", "r"], flat: true };
        if (n.f && n.f[o.k] && (o.k === "p" || o.k === "r" || o.k === "s")) {
            for (k in n.f) if (k === "p" || k === "r" || k === "s") ks.push(k);
            return { ks: ks, flat: false };
        }
        return null;
    }
    /* the spec with its target replaced by its family (two members' specs are one family when
       these are the same) */
    function canon(str) {
        var S = JSON.parse(str), F = family(S);
        if (F) S.o = { l: S.o.l, fl: F.flat };
        return JSON.stringify(S);
    }

    /* ---- the compiler: straight-line functions per family ----
       main(): every read first (host calls, no try: a value not read yet throws out of it); if
       they are all the same as last time, the outputs computed then; else the formulas. reg():
       every read again, each in its own try, so the engine learns them all in the first run. The
       first run of an evaluation throws out of main at its first read and reg asks for
       everything; the second goes through main in one go. Neither has inner functions (closures
       make every local variable slower in the engine). */
    function Gen(S, F) {
        this.S = S;
        this.F = F;
        this.rl = [];
        this.out = [];
        this.reads = [];
        this.ins = [];
        this.consts = [];
        this.n = 0;
        this.m = {};
        this.kv = {};
        this.chk = {};
        this.v = false;
    }
    var G = Gen.prototype;
    G.t = function (code) {
        var v = "t" + this.n++;
        this.out.push("var " + v + " = " + code + ";");
        return v;
    };
    G.k = function (value) {
        var j = JSON.stringify(value), m = this.m["K" + j];
        if (m) return m;
        var v = "k" + this.consts.length;
        this.consts.push("var " + v + " = " + j + ";");
        this.m["K" + j] = v;
        this.kv[v] = value;
        return v;
    };
    G.memo = function (key, fn) {
        var m = this.m;
        if (m[key] === null) throw new Error("circle");
        if (m[key] === undefined) {
            m[key] = null;
            m[key] = fn.call(this);
        }
        return m[key];
    };
    G.id = function (li) { return this.S.L[li][0]; };
    /* A host request, with the reads at the top of main. A layer or property that isn't there
       (null) sends the evaluation to the walker. cv: a conversion for the formulas (the input
       compared with last time's is what they use); info: a parent check, not an input. */
    G.rd = function (args, cv, info) {
        var v = "t" + this.n++;
        this.reads.push(args);
        this.rl.push("var " + v + " = H(" + args + ");");
        this.rl.push("if (" + v + " === null) return null;");
        if (cv) {
            var w = "t" + this.n++;
            this.rl.push("var " + w + " = " + cv + "(" + v + ");");
            v = w;
        }
        if (!info) this.ins.push(v);
        return v;
    };
    G.read = function (li, path, pre) {
        return this.memo("R" + li + path + (pre ? 1 : 0), function () {
            return this.rd(QV + ", c, " + this.id(li) + ", " + JSON.stringify(path) + ", t, " + (pre ? "true" : "false"));
        });
    };
    /* A node's parent is checked (LINFO) wherever an animator may re-parent it: every node but
       the hidden nulls Rig Kit made under rig layers (zeros, pivots), and a flattened layer when
       it is the target. On a change the walker reads the real parent. */
    G.node = function (li) {
        var n = this.S.N[li];
        if (!n) return null;
        var fl = n.ap !== undefined;
        if (!this.chk[li] && (li === this.S.o.l || (!fl && !(n.c && n.p >= 0 && this.S.N[n.p])))) {
            this.chk[li] = 1;
            var par = fl ? n.ap : n.p;
            var iv = this.rd(QI + ", c, " + this.id(li), null, true);
            this.rl.push("if (" + iv + "[7] !== " + (par < 0 ? "null" : String(this.id(par))) + ") return null;");
        }
        return n;
    };
    /* one of the outputs (the target's property, or its family's) */
    G.out1 = function (li, k) {
        var o = this.S.o;
        if (li !== o.l) return false;
        if (!this.F) return k === o.k;
        for (var i = 0; i < this.F.ks.length; i++) if (this.F.ks[i] === k) return true;
        return false;
    };
    /* the value under a property's expression: the expression's own value (v) when the spec has
       one output, read otherwise (family members share the code) */
    G.pre = function (li, k) {
        var o = this.S.o;
        if (!this.F && li === o.l && k === o.k) {
            this.v = true;
            return "v";
        }
        return this.read(li, PATH[k], true);
    };
    G.prop = function (li, k) {
        return this.memo("P" + k + li, function () {
            var n = this.node(li), f = n && n.f && n.f[k];
            if (f) return this.calc(li, k, f);
            if (n && n.c && n.c[k] !== undefined) return this.k(n.c[k]);
            /* a flattened layer's own position and rotation are its rest values under its virtual
               parent: its static values (its expressions show the result) */
            if (n && n.ap !== undefined && (k === "p" || k === "r")) return this.pre(li, k);
            return this.read(li, PATH[k], false);
        });
    };
    G.anchor = function (li, n) {
        return n && n.c && n.c.a ? this.k(n.c.a) : this.read(li, PATH.a, false);
    };
    G.world = function (li) {
        if (li < 0) return "ID";
        return this.memo("W" + li, function () {
            var n = this.node(li);
            if (n) {
                var W = this.world(n.p), q = [this.prop(li, "p"), this.prop(li, "r"), this.prop(li, "s"), this.anchor(li, n)], kv = this.kv;
                if (q[0] in kv && q[1] in kv && q[2] in kv && q[3] in kv) {
                    var M = this.k(mat(kv[q[0]], kv[q[1]], kv[q[2]], kv[q[3]]));
                    return W === "ID" ? M : this.t("mul(" + W + ", " + M + ")");
                }
                return this.t((W === "ID" ? "mat(" : "mm(" + W + ", ") + q.join(", ") + ")");
            }
            return this.rd(QX + ", c, " + this.id(li) + ", t", "m4x");
        });
    };
    G.pworld = function (li) {
        var n = this.node(li);
        if (!n) throw new Error("not a node");
        return this.world(n.p);
    };
    G.aw = function (li) {
        return this.memo("A" + li, function () {
            if (this.node(li)) return this.t("ap(" + this.pworld(li) + ", " + this.prop(li, "p") + ")");
            return this.t("ap(" + this.world(li) + ", " + this.read(li, PATH.a, false) + ")");
        });
    };
    G.fx = function (f) {
        if (!f) return "0";
        var n = f[1], safe = n.indexOf("/") < 0 && n.indexOf(".") < 0;
        return this.memo("F" + f[0] + "|" + n, function () {
            return this.rd(QV + ", c, " + this.id(f[0]) + ", " + JSON.stringify(safe ? "effects/" + n + "/#1" : "effects/@" + f[2] + "/#1") + ", t, false", "num");
        });
    };
    G.solve = function (qi) {
        return this.memo("Q" + qi, function () {
            var q = this.S.Q[qi], sc = [], g = [], i;
            for (i = 0; i < q.b.length; i++) sc.push(this.prop(q.b[i], "s"));
            for (i = 0; i < q.g.length; i++) g.push(this.fx(q.g[i]));
            return this.t("solve(" + this.pworld(q.b[0]) + ", " + this.prop(q.b[0], "p") + ", [" + sc.join(", ") + "], " + this.aw(q.c) + ", [" + g.join(", ") +
                "], " + this.fx(q.l) + ", " + this.k(q.A) + ", " + this.k(q.E) + ")");
        });
    };
    G.curve = function (zi) {
        return this.memo("Z" + zi, function () {
            var z = this.S.B[zi];
            return this.t("curve(" + this.aw(z.e) + ", " + this.aw(z.r) + ", " + this.aw(z.k1) + ", " + (z.k2 >= 0 ? this.aw(z.k2) : "null") + ", " +
                this.fx(z.o) + ", " + this.fx(z.a) + ")");
        });
    };
    G.calc = function (li, k, f) {
        var i;
        switch (f.k) {
        case "lock":
            if (f.v === undefined) return this.read(li, PATH[k], true);
            var lv = f.v, part = false;
            if (typeof lv !== "number") for (i = 0; i < lv.length; i++) if (lv[i] === null) part = true;
            return part ? this.t("lk(" + this.read(li, PATH[k], true) + ", " + this.k(lv) + ")") : this.k(lv);
        case "look":
            var fk = this.fx(f.f);
            if (f.x) for (i = 0; i < f.x.length; i++) fk += " + " + this.fx(f.x[i]);
            return this.t("look(" + this.pworld(li) + ", ap(" + this.pworld(li) + ", " + this.prop(li, "p") + "), " + this.aw(f.c) + ", " + this.fx(f.v) + ", " +
                this.fx(f.w) + ", " + fk + ", " + this.pre(li, k) + ")");
        case "goal":
            return this.t("goal(" + this.pworld(li) + ", " + this.fx(f.s) + ", " + (f.c >= 0 ? this.world(f.c) + ", " + this.prop(f.c, "r") : "null, 0") +
                ", " + this.pre(li, k) + ")");
        case "ik":
            var q = this.S.Q[f.q], pre = f.o.charAt(0) === "r" || (f.o !== "ep" && !this.out1(li, k)) ? "null" : this.pre(li, k);
            return this.t("ikOut(" + this.solve(f.q) + ", " + JSON.stringify(f.o) + ", " + this.k(q.A) + ", " + this.k(q.E) + ", " + (f.e ? this.k(f.e) : "null") + ", " + pre + ")");
        case "bz":
            var z = this.S.B[f.b], Z = this.curve(f.b);
            if (f.o === "p") return this.t("bzPos(" + Z + ", " + z.t[f.i] + ", " + this.fx(z.w[f.i]) + ", " + this.k(z.p[f.i]) + ", " + this.pre(li, k) + ")");
            return this.t("bzRot(" + Z + ", " + this.aw(li) + ", " + (f.i > 0 ? this.aw(z.b[f.i - 1]) : Z + ".e") + ", " + this.pre(li, k) + ")");
        case "roll":
            return this.t("roll(" + JSON.stringify(f.m) + ", " + this.fx(f.t) + ", " + this.fx(f.h) + ", " + this.fx(f.r) + ", " + this.pre(li, k) + ")");
        case "apos":
            return this.t("apos(" + this.pworld(li) + ", " + this.aw(f.c) + ", " + this.fx(f.w) + ", " + this.k(f.l) + ", " + this.pre(li, k) + ")");
        case "neck":
            return this.t("neck(" + this.prop(f.t, "r") + (f.h >= 0 ? " + " + this.prop(f.h, "r") : "") + ", " + f.t0 + ", " + this.prop(f.s, "r") + ", " + f.n +
                ", " + this.fx(f.o) + ", " + this.pre(li, k) + ")");
        case "flat":
            var nd = this.node(li);
            if (!nd || nd.ap === undefined) throw new Error("not flat");
            return this.t("flat(" + this.world(li) + ", " + (nd.ap >= 0 ? this.world(nd.ap) : "ID") + ", " + this.anchor(li, nd) + ", " + this.prop(li, "s") +
                ", " + JSON.stringify(k) + ", " + this.pre(li, k) + ")");
        }
        throw new Error("unknown formula " + f.k);
    };
    /* The function of a spec's family: fn(H, c, t, v, E) gives the outputs ({p: ..., r: ...}),
       1 while the engine still has reads to answer, 2 when the spec doesn't fit the comp (a
       layer gone, a parent changed): the walker's turn. E keeps the last inputs and outputs. */
    function compile(S) {
        var o = S.o;
        if (!H || !S.N[o.l]) return null;
        var F = family(S), g = new Gen(S, F), outs = [], i;
        try {
            if (!F) outs.push(JSON.stringify(o.k) + ": " + g.calc(o.l, o.k, o.f));
            else {
                for (i = 0; i < F.ks.length; i++) {
                    var k = F.ks[i];
                    outs.push(k + ": " + (F.flat ? g.calc(o.l, k, o.f) : g.prop(o.l, k)));
                }
            }
        } catch (e) {
            return null;
        }
        if (g.v) g.ins.push("v");
        var reg = [];
        for (i = 0; i < g.reads.length; i++) reg.push("try { H(" + g.reads[i] + "); } catch (x) {}");
        var src = "var " + NAMES.join(", ") + ";\n" + g.consts.join("\n") +
            "\nfunction reg(H, c, t) {\n" + reg.join("\n") + "\n}\n" +
            "function main(H, c, t, v, E) {\n" + g.rl.join("\n") + "\nvar I = [" + g.ins.join(", ") + "];\n" +
            "if (E.i && same(E.i, I)) return E.o;\n" +
            g.out.join("\n") + "\nE.i = I;\nE.o = {" + outs.join(", ") + "};\nreturn E.o;\n}\n" +
            "return function (H, c, t, v, E) {\n" +
            "try {\nreturn main(H, c, t, v, E) || 2;\n} catch (x) {\n" +
            "if (String(x && x.message).charCodeAt(0) !== 1) throw x;\n" +
            "reg(H, c, t);\nreturn 1;\n}\n};";
        try {
            return new Function("L", src)(LIB);
        } catch (e) {
            return null;
        }
    }

    /* ---- the walker: the same formulas, reading what it needs as it goes (a layer renamed in a
       duplicated comp, a parent changed since the rig was built, a 3D layer) ---- */
    function Ctx(S, comp, t, v) {
        this.S = S;
        this.comp = comp;
        this.t = t;
        this.v = v;
        this.c = comp ? comp.__c : undefined;
        this.fast = !!H && this.c !== undefined && this.c !== null;
        this.wait = false;
        this.ids = [];
        this.inf = [];
        this.ok = [];
        this.W = [];
        this.A = [];
        this.P = {};
        this.q = [];
        this.z = [];
        this.lays = [];
    }
    var C = Ctx.prototype;
    C.err = function (m) { throw new Error("Rig Kit: " + m); };
    C.name = function (li) { return this.S.L[li][1]; };
    C.h = function (op, a, b, c, d, e) {
        try {
            return H(op, a, b, c, d, e);
        } catch (x) {
            if (String(x && x.message).charCodeAt(0) === 1) {
                this.wait = true;
                return undefined;
            }
            throw x;
        }
    };
    C.id = function (li) {
        var x = this.ids[li];
        return x !== undefined ? x : this.S.L[li][0];
    };
    /* The spec's id didn't answer: find the layer by its name. True when another id was found. */
    C.relook = function (li) {
        if (this.ids[li] !== undefined) return false;
        var e = this.S.L[li], r;
        /* in a copy of the comp the rig was written in: the name the layer has there now (it may
           have been renamed since) */
        if (this.S.c !== undefined && this.S.c !== this.c) {
            var oi = this.h(QI, this.S.c, e[0]);
            if (oi === undefined) return false;
            if (oi && oi[0] !== e[1]) {
                r = this.h(QL, this.c, oi[0]);
                if (r === undefined) return false;
                if (r !== null) {
                    this.ids[li] = r;
                    return r !== e[0];
                }
            }
        }
        r = this.h(QL, this.c, e[1]);
        if (r === undefined) return false;
        if (r === null) this.err("layer '" + e[1] + "' not found");
        this.ids[li] = r;
        return r !== e[0];
    };
    C.info = function (li) {
        var x = this.inf[li];
        if (x !== undefined) return x;
        var r = this.h(QI, this.c, this.id(li));
        if (r === null) return this.relook(li) ? this.info(li) : undefined;
        if (r !== undefined) {
            this.inf[li] = r;
            if (this.ids[li] === undefined) this.ids[li] = this.S.L[li][0];
        }
        return r;
    };
    C.val = function (li, path, pre) {
        if (!this.fast) return this.slow(li, path);
        var r = this.h(QV, this.c, this.id(li), path, this.t, !!pre);
        if (r === null) {
            if (this.relook(li)) return this.val(li, path, pre);
            if (!this.wait) this.err("'" + path + "' not found on layer '" + this.name(li) + "'");
        }
        return r;
    };
    /* An effect's first parameter: f = [layer index, effect name, effect uid]. */
    C.fx = function (f) {
        if (!f) return 0;
        var li = f[0], n = f[1];
        if (!this.fast) return num(this.lay(li).effect(n)(1).valueAtTime(this.t));
        var safe = n.indexOf("/") < 0 && n.indexOf(".") < 0;
        var r = this.h(QV, this.c, this.id(li), safe ? "effects/" + n + "/#1" : "effects/@" + f[2] + "/#1", this.t, false);
        if (r === null) {
            if (this.relook(li)) return this.fx(f);
            if (this.wait) return 0;
            var ch = this.h(QC, this.c, this.id(li), "effects", n);
            if (ch === undefined) return 0;
            if (ch === null) this.err("effect '" + n + "' not found on layer '" + this.name(li) + "'");
            r = this.h(QV, this.c, this.id(li), ch[0] + "/#1", this.t, false);
        }
        return num(r);
    };
    /* The node of layer li when its world transform is computed here (its parent unchanged). */
    C.node = function (li) {
        if (!this.fast) return null;
        var n = this.S.N[li];
        if (!n) return null;
        /* a flattened layer's virtual parent doesn't depend on its actual one (the flat output
           reads the actual one itself) */
        if (n.ap !== undefined) return n;
        var ok = this.ok[li];
        if (ok !== undefined) return ok ? n : null;
        var inf = this.info(li);
        if (inf === undefined) return n;
        var par = inf[7] === undefined ? null : inf[7], want = null;
        if (n.p >= 0) {
            if (this.info(n.p) === undefined) return n;
            want = this.id(n.p);
        }
        ok = par === want;
        this.ok[li] = ok;
        return ok ? n : null;
    };
    C.pre = function (li, k) {
        var o = this.S.o;
        if (li === o.l && k === o.k) return this.v;
        return this.val(li, PATH[k], true);
    };
    C.prop = function (li, k) {
        var key = k + li, P = this.P;
        if (key in P) {
            if (P[key] === null) this.err("the rig reads itself in a circle (" + this.name(li) + ")");
            return P[key];
        }
        P[key] = null;
        var n = this.node(li), f = n && n.f && n.f[k], r;
        if (f) r = this.calc(li, k, f);
        else if (n && n.c && n.c[k] !== undefined) r = n.c[k];
        else if (n && n.ap !== undefined && (k === "p" || k === "r")) r = this.pre(li, k);
        else r = this.val(li, PATH[k], false);
        r = k === "r" ? num(r) : vec(r);
        P[key] = r;
        return r;
    };
    C.world = function (li) {
        if (li < 0) return ID;
        var w = this.W[li];
        if (w === 0) this.err("the rig reads itself in a circle (" + this.name(li) + ")");
        if (w) return w;
        this.W[li] = 0;
        var n = this.node(li);
        if (n) w = mul(this.world(n.p), mat(this.prop(li, "p"), this.prop(li, "r"), this.prop(li, "s"), n.c && n.c.a ? n.c.a : vec(this.val(li, PATH.a, false))));
        else if (this.fast) {
            var r = this.h(QX, this.c, this.id(li), this.t);
            if (r === null && this.relook(li)) {
                this.W[li] = undefined;
                return this.world(li);
            }
            w = m4x(r);
        } else w = this.slowWorld(this.lay(li));
        this.W[li] = w;
        return w;
    };
    C.pworld = function (li) {
        var n = this.node(li);
        if (n) return this.world(n.p);
        if (!this.fast) {
            var L = this.lay(li);
            return L.hasParent ? this.slowWorld(L.parent) : ID;
        }
        var inf = this.info(li);
        if (!inf) return ID;
        var par = inf[7];
        return par === null || par === undefined ? ID : m4x(this.h(QX, this.c, par, this.t));
    };
    C.aw = function (li) {
        var a = this.A[li];
        if (a) return a;
        if (this.node(li)) a = ap(this.pworld(li), this.prop(li, "p"));
        else a = ap(this.world(li), vec(this.val(li, PATH.a, false)));
        this.A[li] = a;
        return a;
    };
    C.lay = function (li) {
        var L = this.lays[li];
        if (!L) L = this.lays[li] = this.comp.layer(this.name(li));
        return L;
    };
    C.slow = function (li, path) {
        var tr = this.lay(li).transform;
        var p = path === PATH.p ? tr.position : path === PATH.r ? tr.rotation : path === PATH.s ? tr.scale : tr.anchorPoint;
        return p.valueAtTime(this.t);
    };
    C.slowWorld = function (L) {
        var t = this.t, o = L.toWorld([0, 0, 0], t), x = L.toWorldVec([1, 0, 0], t), y = L.toWorldVec([0, 1, 0], t);
        return [x[0], y[0], x[1], y[1], o[0], o[1]];
    };
    /* 3D layers (by layer id) */
    C.info3 = function (id) {
        var r = this.h(QI, this.c, id);
        return r || null;
    };
    C.is3 = function (li) {
        if (!this.fast) return false;
        var inf = this.info(li);
        return !!(inf && inf[9]);
    };
    /* the plane frame of a 3D layer: [F, F inverse] = its parent's world, its position, its
       Orientation (it turns about Z in there) */
    C.frame3 = function (id) {
        var inf = this.info3(id), par = inf ? inf[7] : null, P = I4, Pi = I4;
        if (par !== null && par !== undefined) {
            var x = this.h(QX, this.c, par, this.t);
            if (x) {
                P = x[2];
                Pi = x[3];
            }
        }
        var p = vec(this.h(QV, this.c, id, PATH.p, this.t, false)), o = vec(this.h(QV, this.c, id, "transform/orientation", this.t, false));
        var R = rot4(o), z = p.length > 2 ? p[2] : 0;
        return [m4m(P, m4m(mv4(p), R)), m4m(tr4(R), m4m(mv4([-p[0], -p[1], -z]), Pi))];
    };
    /* a layer's anchor point in a plane frame (Fi: the frame's inverse) */
    C.at3 = function (Fi, li) {
        var x = this.h(QX, this.c, this.id(li), this.t);
        if (!x) return [0, 0];
        var q = m4p(Fi, m4p(x[2], vec(this.val(li, PATH.a, false))));
        return [q[0], q[1]];
    };
    /* IK Goal on a 3D layer: angles in the plane of its chain's root (up its parents while they
       are 3D with no Orientation) */
    C.goal3 = function (li, f, pre) {
        var inf = this.info(li), par = inf ? inf[7] : null;
        if (par === null || par === undefined) return goal(ID, this.fx(f.s), null, 0, pre);
        var id = par, guard = 0;
        while (guard++ < 64) {
            var o = vec(this.h(QV, this.c, id, "transform/orientation", this.t, false));
            if (Math.abs(o[0]) > 1e-6 || Math.abs(o[1]) > 1e-6 || Math.abs(o[2]) > 1e-6) break;
            var pi = this.info3(id), up = pi ? pi[7] : null;
            if (up === null || up === undefined) break;
            var ui = this.info3(up);
            if (!ui || !ui[9]) break;
            id = up;
        }
        var F3 = this.frame3(id), xp = this.h(QX, this.c, par, this.t);
        var W = xp ? m4(m4m(F3[1], xp[2])) : ID;
        var Wc = null;
        if (f.c >= 0) {
            /* the controller's turn in the plane (it needn't lie in it) */
            var xc = this.h(QX, this.c, this.id(f.c), this.t), tw = xc ? turn3(F3[0], xc[2]) * DEG : 0;
            Wc = [Math.cos(tw), -Math.sin(tw), Math.sin(tw), Math.cos(tw), 0, 0];
        }
        return goal(W, this.fx(f.s), Wc, f.c >= 0 ? this.prop(f.c, "r") : 0, pre);
    };
    C.solve = function (qi) {
        var R = this.q[qi];
        if (R === 0) this.err("an IK controller is a child of its own bones");
        if (R) return R;
        this.q[qi] = 0;
        var q = this.S.Q[qi], sc = [], g = [], i;
        for (i = 0; i < q.b.length; i++) sc.push(this.prop(q.b[i], "s"));
        for (i = 0; i < q.g.length; i++) g.push(this.fx(q.g[i]));
        if (this.is3(q.b[0])) {
            /* a 3D chain: in its root's plane, where the root sits at the origin */
            var F3 = this.frame3(this.id(q.b[0]));
            R = solve(ID, [0, 0, 0], sc, this.at3(F3[1], q.c), g, this.fx(q.l), q.A, q.E);
        } else R = solve(this.pworld(q.b[0]), this.prop(q.b[0], "p"), sc, this.aw(q.c), g, this.fx(q.l), q.A, q.E);
        this.q[qi] = R;
        return R;
    };
    C.curve = function (zi) {
        var Z = this.z[zi];
        if (Z === 0) this.err("a curve controller is a child of its own bones");
        if (Z) return Z;
        this.z[zi] = 0;
        var z = this.S.B[zi];
        Z = curve(this.aw(z.e), this.aw(z.r), this.aw(z.k1), z.k2 >= 0 ? this.aw(z.k2) : null, this.fx(z.o), this.fx(z.a));
        this.z[zi] = Z;
        return Z;
    };
    C.calc = function (li, k, f) {
        var i;
        switch (f.k) {
        case "lock":
            if (f.v === undefined) return this.val(li, PATH[k], true);
            return typeof f.v === "number" ? f.v : lk(this.val(li, PATH[k], true), f.v);
        case "look":
            var fk = this.fx(f.f);
            if (f.x) for (i = 0; i < f.x.length; i++) fk += this.fx(f.x[i]);
            if (this.is3(li)) {
                var F3 = this.frame3(this.id(li));
                return look(ID, [0, 0], this.at3(F3[1], f.c), this.fx(f.v), this.fx(f.w), fk, num(this.pre(li, k)));
            }
            var W = this.pworld(li);
            return look(W, ap(W, this.prop(li, "p")), this.aw(f.c), this.fx(f.v), this.fx(f.w), fk, num(this.pre(li, k)));
        case "goal":
            if (this.is3(li)) return this.goal3(li, f, num(this.pre(li, k)));
            return goal(this.pworld(li), this.fx(f.s), f.c >= 0 ? this.world(f.c) : null, f.c >= 0 ? this.prop(f.c, "r") : 0, num(this.pre(li, k)));
        case "ik":
            var q = this.S.Q[f.q];
            return ikOut(this.solve(f.q), f.o, q.A, q.E, f.e, f.o.charAt(0) === "r" ? null : this.pre(li, k));
        case "bz":
            var z = this.S.B[f.b], Z = this.curve(f.b);
            if (f.o === "p") return bzPos(Z, z.t[f.i], this.fx(z.w[f.i]), z.p[f.i], this.pre(li, k));
            return bzRot(Z, this.aw(li), f.i > 0 ? this.aw(z.b[f.i - 1]) : Z.e, num(this.pre(li, k)));
        case "roll": return roll(f.m, this.fx(f.t), this.fx(f.h), this.fx(f.r), num(this.pre(li, k)));
        case "apos": return apos(this.pworld(li), this.aw(f.c), this.fx(f.w), f.l, this.pre(li, k));
        case "neck":
            return neck(this.prop(f.t, "r") + (f.h >= 0 ? this.prop(f.h, "r") : 0), f.t0, this.prop(f.s, "r"), f.n, this.fx(f.o), num(this.pre(li, k)));
        case "flat":
            var nd = this.S.N[li], Wa;
            if (!nd || nd.ap === undefined) this.err("not a flattened layer");
            /* without host reads the static values can't be read: show them as they are */
            if (!this.fast) return this.pre(li, k);
            var inf = this.info(li), par = inf ? inf[7] : undefined;
            if (par === undefined) Wa = nd.ap >= 0 ? this.world(nd.ap) : ID;
            else if (par === null) Wa = ID;
            else if (nd.ap >= 0 && par === this.id(nd.ap)) Wa = this.world(nd.ap);
            else Wa = m4x(this.h(QX, this.c, par, this.t));
            var an = nd.c && nd.c.a ? nd.c.a : vec(this.val(li, PATH.a, false));
            return flat(this.world(li), Wa, an, this.prop(li, "s"), k, this.pre(li, k));
        }
        this.err("unknown formula " + f.k);
    };

    var stats = { compiled: 0, walked: 0, failed: 0, cached: 0 };
    /* The entry of a spec: by its family key (short: a long string is slow to look up). Another
       member of the family joins once its spec is checked to be the same but for its target. */
    function entry(str, fk) {
        var E = fams[fk], i;
        if (E) {
            if (E.s === str) return E;
            for (i = 0; i < E.m.length; i++) if (E.m[i] === str) return E;
            if (E.c === undefined) E.c = canon(E.s);
            if (canon(str) === E.c) {
                E.m.push(str);
                return E;
            }
            /* two families, one key: this one goes on its own */
            fk = fk + "|" + str;
            E = fams[fk];
            if (E) return E;
        }
        if (count > 3000) {
            fams = {};
            count = 0;
        }
        count++;
        /* specs are parsed again for the walker only (fewer objects for the engine's garbage
           collector to go through) */
        return (fams[fk] = { s: str, c: undefined, m: [], fn: undefined, i: null, o: null, bad: undefined, W: null });
    }
    /* str: the spec (JSON); fk: its family key; k: the output wanted ("p", "r", "s" or "x") */
    function run(str, comp, t, v, fk, k) {
        var E = entry(str, fk || str), c = comp ? comp.__c : undefined;
        if (c !== undefined && c !== null && H) {
            if (E.fn === undefined) E.fn = compile(JSON.parse(str));
            if (E.fn && E.bad !== c) {
                var o0 = E.o, r = E.fn(H, c, t, v, E);
                if (typeof r === "object") {
                    if (r === o0) stats.cached++;
                    else stats.compiled++;
                    return r[k];
                }
                if (r === 1) return v;
                E.bad = c;
                stats.failed++;
            }
        }
        /* the walker (the member's own spec: its target) */
        var W = E.W || (E.W = {}), S = W[k];
        if (!S || S.str !== str) S = W[k] = { str: str, S: JSON.parse(str) };
        var X = new Ctx(S.S, comp, t, v), o = S.S.o;
        var res = X.calc(o.l, o.k, o.f);
        if (!X.wait) stats.walked++;
        return X.wait ? v : res;
    }
    return { run: run, version: 3, stats: stats, compile: compile };
};

// ---------------------------------------------------------------------------- the library text

// Strip comments and indentation (the library has no "//" or "/*" inside strings).
RK.rkx.minify = function (src) {
    var out = "", i = 0, n = src.length, q = null;
    while (i < n) {
        var ch = src.charAt(i), nx = src.charAt(i + 1);
        if (q) {
            out += ch;
            if (ch === "\\") {
                out += nx;
                i += 2;
                continue;
            }
            if (ch === q) q = null;
            i++;
            continue;
        }
        if (ch === "\"" || ch === "'") {
            q = ch;
            out += ch;
            i++;
            continue;
        }
        if (ch === "/" && nx === "/") {
            while (i < n && src.charAt(i) !== "\n") i++;
            continue;
        }
        if (ch === "/" && nx === "*") {
            var e = src.indexOf("*/", i + 2);
            i = e < 0 ? n : e + 2;
            out += " ";
            continue;
        }
        out += ch;
        i++;
    }
    var lines = out.split("\n"), keep = [];
    for (var k = 0; k < lines.length; k++) {
        var l = RK.rkx.squeeze(lines[k].replace(/^\s+/, "").replace(/\s+$/, ""));
        if (l) keep.push(l);
    }
    return keep.join("\n");
};

// Spaces next to punctuation go (outside strings), except where "+ +" or "- -" would merge.
RK.rkx.squeeze = function (l) {
    var out = "", q = null, i, word = /[A-Za-z0-9_$]/;
    for (i = 0; i < l.length; i++) {
        var ch = l.charAt(i);
        if (q) {
            out += ch;
            if (ch === "\\") {
                out += l.charAt(++i);
                continue;
            }
            if (ch === q) q = null;
            continue;
        }
        if (ch === "\"" || ch === "'") {
            q = ch;
            out += ch;
            continue;
        }
        if (ch === " ") {
            var a = out.charAt(out.length - 1), j = i;
            while (l.charAt(j) === " ") j++;
            var b = l.charAt(j);
            if ((word.test(a) && word.test(b)) || (a === "+" && b === "+") || (a === "-" && b === "-")) out += " ";
            i = j - 1;
            continue;
        }
        out += ch;
    }
    return out;
};

RK.rkx.hash = function (s) {
    var h = 0x811c9dc5;
    for (var i = 0; i < s.length; i++) {
        h ^= s.charCodeAt(i);
        h = (h + ((h << 1) + (h << 4) + (h << 7) + (h << 8) + (h << 24))) >>> 0;
    }
    return ("0000000" + h.toString(16)).slice(-8);
};

RK.rkx.LIB = "return (" + RK.rkx.minify(RK.rkx.factory.toString()) + ")();";
RK.rkx.G = "__rkx_" + RK.rkx.hash(RK.rkx.LIB);
RK.rkx.HEAD = "var __rkxL = typeof " + RK.rkx.G + " === \"object\" && " + RK.rkx.G + " ? " + RK.rkx.G + " : (" + RK.rkx.G +
    " = new Function(" + JSON.stringify(RK.rkx.LIB) + ")());\n";
RK.rkx.CALL = "__rkxL.run(";

// 64 bits (two lanes) of a string: the key of a spec's family.
RK.rkx.hash64 = function (s) {
    var a = 5381, b = 52711;
    for (var i = 0; i < s.length; i++) {
        var ch = s.charCodeAt(i);
        a = (Math.imul(a, 33) ^ ch) >>> 0;
        b = (Math.imul(b, 31) + ch) >>> 0;
    }
    return ("0000000" + a.toString(16)).slice(-8) + ("0000000" + b.toString(16)).slice(-8);
};

// The key of a spec's family (the library's family(): a layer's position and rotation computed
// together share one compiled function and one cache): the spec with its target replaced by the
// family. The library checks the specs themselves before sharing, so the key only finds them.
RK.rkx.fkey = function (spec) {
    var o = spec.o, n = spec.N[o.l], c = o;
    if (n && o.f && o.f.k === "flat") c = { l: o.l, fl: true };
    else if (n && n.f && n.f[o.k] && (o.k === "p" || o.k === "r" || o.k === "s")) c = { l: o.l, fl: false };
    return RK.rkx.hash64(JSON.stringify({ L: spec.L, N: spec.N, Q: spec.Q, B: spec.B, c: spec.c, o: c }));
};

RK.rkx.text = function (label, spec) {
    return RK.TAG + label + "\n" + RK.rkx.HEAD + RK.rkx.CALL + JSON.stringify(JSON.stringify(spec)) + ", thisComp, time, value, \"" +
        RK.rkx.fkey(spec) + "\", " + JSON.stringify(spec.o.k) + ")\n";
};

RK.rkx.isRkx = function (text) {
    return typeof text === "string" && text.indexOf(RK.rkx.CALL) >= 0 && text.indexOf(RK.TAG) === 0;
};

// The spec inside an RKX expression's text, or null.
RK.rkx.parse = function (text) {
    if (!RK.rkx.isRkx(text)) return null;
    var i = text.lastIndexOf(RK.rkx.CALL) + RK.rkx.CALL.length;
    if (text.charAt(i) !== "\"") return null;
    var j = i + 1;
    while (j < text.length) {
        var ch = text.charAt(j);
        if (ch === "\\") {
            j += 2;
            continue;
        }
        if (ch === "\"") break;
        j++;
    }
    try {
        return JSON.parse(JSON.parse(text.substring(i, j + 1)));
    } catch (e) {
        return null;
    }
};

// Layers renamed: the names an RKX expression keeps for its layers (used when a comp is
// duplicated and the ids change). names: {layer id: new name}. Returns the text, changed or not.
RK.rkx.renameIn = function (text, names) {
    var S = RK.rkx.parse(text);
    if (!S) return text;
    var hit = false;
    for (var i = 0; i < S.L.length; i++) {
        var nn = names[S.L[i][0]];
        if (nn !== undefined && nn !== S.L[i][1]) {
            S.L[i][1] = nn;
            hit = true;
        }
    }
    if (!hit) return text;
    return RK.rkx.text(text.substring(RK.TAG.length, text.indexOf("\n")), S);
};

// ---------------------------------------------------------------------------- formulas (script side)
//
// Formulas name layers (Layer objects) and effect parameters (RK.rkx.fx refs):
//   look  {c: controller, w: Weight, v: Reverse, f: FK, x: [more FK terms]}         rotation
//   goal  {c: controller or null, s: switch}                                        rotation
//   ik    {q: solver, o: "r0".."r2" | "p1" | "p2" | "gp" | "ep", e: [effect offset]}
//   bz    {b: curve, i: bone index, o: "p" | "r"}                                    position, rotation
//   roll  {m: mode, t: Tiptoe, h: Heel, r: Foot roll}                                rotation
//   apos  {c: controller, w: share (%), l: the controller in the parent's space at rig time}
//   neck  {s: shoulders controller, o: switch, t: torso, h: hips controller or null, t0, n}
// solver {b: [bones, root first], c: controller, g: [Clockwise, Disable IK, Upper FK, (Middle FK),
//         Lower FK, Auto-Stretch, Stretch, Auto-Shrink], l: IK Limit, A: anchors, E: ends}
// curve  {e, r, k1, k2: layers (k2 null), o: Offset, a: Auto Orientation, b: [bones, tip first],
//         w: [each bone's own offset], t: [t0], p: [[x, y] rest positions]}

RK.rkx.fx = function (layer, name) {
    var e = RK.findEffect(layer, name);
    return { layer: layer, name: name, uid: e ? e.__uid : 0 };
};

RK.rkx.ADDITIVE = { look: true, goal: true, bz: true, roll: true, apos: true, neck: true };
RK.rkx.R6 = function (v) {
    if (v instanceof Array) {
        var o = [];
        for (var i = 0; i < v.length; i++) o.push(RK.rkx.R6(v[i]));
        return o;
    }
    return Math.round(v * 1e6) / 1e6;
};

// ---------------------------------------------------------------------------- the builder

RK.rkx.Builder = function (comp) {
    this.comp = comp;
    this.flats = {};      // layer id -> {layer, vp: its virtual parent, done}: limbs to flatten
    this.limbs = [];      // [[layers, top first]]
    this.vpImp = {};      // layer id -> virtual parent (Layer or null) of a layer flattened earlier
    this.clears = [];     // properties whose expression goes
    this.F = {};          // layer id -> {p, r, s}: transform formulas registered here
    this.targets = [];    // {layer, k, prop, f, label}
    this.memo = {};       // per commit: isNode
    this.imp = {};        // layer id -> imported formulas {p, r, s}
    this.solvers = {};    // imported solvers and curves, by key
    this.byId = null;
};

// Register the formula of a property. k: "p" | "r" | "s" for the transform, "x" for anything
// else (prop then given).
RK.rkx.Builder.prototype.set = function (layer, k, f, label, prop) {
    if (k !== "x") this.unflat(layer);
    var p = prop || (k === "p" ? layer.transform.position : k === "r" ? RK.rotProp(layer) : layer.transform.scale);
    for (var i = 0; i < this.targets.length; i++) {
        var t = this.targets[i];
        if (t.layer.id === layer.id && t.k === k && t.prop.__uid === p.__uid) this.targets.splice(i--, 1);
    }
    this.targets.push({ layer: layer, k: k, prop: p, f: f, label: label || f.k });
    if (k !== "x") {
        var e = this.F[layer.id] || (this.F[layer.id] = {});
        e[k] = f;
    }
    this.memo = {};
};

// Flatten a limb when the expressions are written (Auto-Rig): its layers, top first, are
// parented straight to the first layer above the limb that no rig formula moves (or to nothing),
// and their expressions show the transform the chain gives them. Their static values keep their
// meaning under their virtual (original) parents. EffectCraft evaluates every parent's
// expressions each time it draws a layer, so a limb five layers deep cost five times its own
// expressions; flat, each layer costs its own.
RK.rkx.Builder.prototype.flatten = function (layers) {
    var limb = [];
    for (var i = 0; i < layers.length; i++) {
        var L = layers[i];
        if (!L || this.flats[L.id] || !RK.rkx.flat(L)) continue;
        this.flats[L.id] = { layer: L, vp: L.parent, done: false };
        limb.push(L);
    }
    if (limb.length) this.limbs.push(limb);
};

// The parent the rig computes a layer under: its virtual parent when it was flattened.
RK.rkx.Builder.prototype.parentOf = function (layer) {
    var f = this.flats[layer.id];
    if (f) return f.vp;
    this.imported(layer);
    var v = this.vpImp[layer.id];
    return v !== undefined ? v : layer.parent;
};
RK.rkx.Builder.prototype.isFlat = function (layer) {
    var f = this.flats[layer.id];
    if (f) return !!f.done;
    this.imported(layer);
    return this.vpImp[layer.id] !== undefined;
};

// A layer flattened earlier that a tool rigs again: back under its virtual parent (its static
// values are already relative to it), its virtual formulas written as its own expressions.
RK.rkx.Builder.prototype.unflat = function (layer) {
    if (this.flats[layer.id] || !this.isFlat(layer)) return;
    var vp = this.vpImp[layer.id], fs = this.imported(layer), self = this;
    RK.withUnlocked(layer, function () {
        app.run("layer.setParent", { comp: self.comp.id, layers: [layer.id], parent: vp ? vp.id : null, compensate: false });
    });
    delete this.vpImp[layer.id];
    this.memo = {};
    var props = { p: layer.transform.position, r: RK.rotProp(layer) };
    for (var k in props) {
        if (fs[k]) this.set(layer, k, fs[k], "rig " + (k === "p" ? "position" : "rotation"));
        else this.clears.push({ layer: layer, prop: props[k] });
    }
};

RK.rkx.Builder.prototype.layerById = function (id) {
    if (!this.byId) {
        this.byId = {};
        for (var i = 1; i <= this.comp.numLayers; i++) {
            var l = this.comp.layer(i);
            this.byId[l.id] = l;
        }
    }
    return this.byId[id] || null;
};
RK.rkx.Builder.prototype.layerByName = function (name) {
    try {
        return this.comp.layer(name) || null;
    } catch (e) {
        return null;
    }
};

// Transform formulas of a layer: registered here, else read back from its rig expressions.
RK.rkx.Builder.prototype.formulas = function (layer) {
    var F = this.F[layer.id] || {}, I = this.imported(layer), out = {};
    var ks = ["p", "r", "s"];
    for (var i = 0; i < ks.length; i++) {
        var k = ks[i];
        if (F[k]) out[k] = F[k];
        else if (I[k]) out[k] = I[k];
    }
    return out;
};

RK.rkx.Builder.prototype.hasFormula = function (layer) {
    var f = this.formulas(layer);
    return !!(f.p || f.r || f.s);
};

RK.rkx.flat = function (layer) {
    try {
        if (RK.is3D(layer) || RK.isCamera(layer)) return false;
        if (typeof LightLayer !== "undefined" && layer instanceof LightLayer) return false;
    } catch (e) {}
    return true;
};

// Is the layer's world transform computed in the expressions (it, or a parent, is rig-driven)?
RK.rkx.Builder.prototype.isNode = function (layer) {
    var m = this.memo, id = layer.id;
    if (m[id] !== undefined) return m[id];
    m[id] = false;
    var vp = this.parentOf(layer);
    var r = RK.rkx.flat(layer) && (this.hasFormula(layer) || (!!vp && this.isNode(vp)));
    m[id] = r;
    return r;
};

// Formulas read back from the layer's rig expressions (rigs built earlier).
RK.rkx.Builder.prototype.imported = function (layer) {
    var c = this.imp[layer.id];
    if (c) return c;
    c = this.imp[layer.id] = {};
    var props = { p: layer.transform.position, r: RK.rotProp(layer), s: layer.transform.scale };
    for (var k in props) {
        var p = props[k], text = "";
        try {
            if (!p || !p.expression) continue;
            if (p.expressionEnabled === false) continue;
            text = p.expression;
        } catch (e) {
            continue;
        }
        if (text.indexOf(RK.TAG + "lock\n") === 0) {
            // a controller's lock: constants instead of some or all of the static values under
            // it; the rig applies them to the static value instead of evaluating the lock
            c[k] = { k: "lock", v: RK.rkx.lockValues(text) };
            continue;
        }
        var S = RK.rkx.parse(text);
        if (!S || !S.o || S.o.k !== k) continue;
        if (S.o.f && S.o.f.k === "flat") {
            // a flattened layer: its expressions carry its virtual parent and formulas
            var n = S.N[S.o.l];
            if (!n) continue;
            if (this.vpImp[layer.id] === undefined) this.vpImp[layer.id] = n.p >= 0 ? this.unpackLayer(S, n.p) : null;
            if (n.f) {
                for (var kk in n.f) {
                    if (c[kk]) continue;
                    var vf = this.unpack(S, n.f[kk]);
                    if (vf) c[kk] = vf;
                }
            }
            continue;
        }
        var f = this.unpack(S, S.o.f);
        if (f) c[k] = f;
    }
    return c;
};

// The constants of a lock expression (RK.ctrl.lock): a number, or one per value with null for
// the values left free ("[value[0], 300]" gives [null, 300]); undefined when it reads otherwise.
RK.rkx.lockValues = function (text) {
    var body = text.substring(text.indexOf("\n") + 1).replace(/^\s+|\s+$/g, "");
    var numRe = /^-?\d+(\.\d+)?(e[-+]?\d+)?$/i;
    if (numRe.test(body)) return parseFloat(body);
    var m = body.match(/^\[(.*)\]$/);
    if (!m) return undefined;
    var parts = m[1].split(","), out = [];
    for (var i = 0; i < parts.length; i++) {
        var q = parts[i].replace(/^\s+|\s+$/g, "");
        if (q === "value[" + i + "]") out.push(null);
        else if (numRe.test(q)) out.push(parseFloat(q));
        else return undefined;
    }
    return out;
};

RK.rkx.Builder.prototype.unpackLayer = function (S, i) {
    var e = S.L[i];
    return e ? this.layerById(e[0]) || this.layerByName(e[1]) : null;
};

// A spec's formula back into script terms (null when a layer it names is gone).
RK.rkx.Builder.prototype.unpack = function (S, f) {
    var self = this, bad = false;
    var lay = function (i) {
        if (i === undefined || i === null || i < 0) return null;
        var e = S.L[i], L = e ? self.layerById(e[0]) || self.layerByName(e[1]) : null;
        if (!L) bad = true;
        return L;
    };
    var fx = function (r) {
        if (!r) return null;
        var L = lay(r[0]);
        return L ? RK.rkx.fx(L, r[1]) : null;
    };
    var solver = function (qi) {
        var q = S.Q[qi];
        var s = { b: q.b.map(lay), c: lay(q.c), g: q.g.map(fx), l: fx(q.l), A: q.A, E: q.E };
        return self.shared("q", s);
    };
    var curve = function (bi) {
        var z = S.B[bi];
        var s = { e: lay(z.e), r: lay(z.r), k1: lay(z.k1), k2: z.k2 >= 0 ? lay(z.k2) : null, o: fx(z.o), a: fx(z.a),
            b: z.b.map(lay), w: z.w.map(fx), t: z.t, p: z.p };
        return self.shared("b", s);
    };
    var out;
    switch (f.k) {
    case "look": out = { k: "look", c: lay(f.c), w: fx(f.w), v: fx(f.v), f: fx(f.f), x: f.x ? f.x.map(fx) : undefined }; break;
    case "goal": out = { k: "goal", c: f.c >= 0 ? lay(f.c) : null, s: fx(f.s) }; break;
    case "ik": out = { k: "ik", q: solver(f.q), o: f.o, e: f.e }; break;
    case "bz": out = { k: "bz", b: curve(f.b), i: f.i, o: f.o }; break;
    case "roll": out = { k: "roll", m: f.m, t: fx(f.t), h: fx(f.h), r: fx(f.r) }; break;
    case "apos": out = { k: "apos", c: lay(f.c), w: fx(f.w), l: f.l }; break;
    case "neck": out = { k: "neck", s: lay(f.s), o: fx(f.o), t: lay(f.t), h: f.h >= 0 ? lay(f.h) : null, t0: f.t0, n: f.n }; break;
    case "flat": out = { k: "flat" }; break;
    case "lock": out = f.v === undefined ? { k: "lock" } : { k: "lock", v: f.v }; break;
    default: return null;
    }
    return bad ? null : out;
};

// One object per solver or curve read back (two outputs of one IK share its solve).
RK.rkx.Builder.prototype.shared = function (kind, s) {
    var key = kind + JSON.stringify(s, function (k, v) {
        if (v && typeof v === "object" && v.id !== undefined && v.containingComp !== undefined) return "L" + v.id;
        if (v && typeof v === "object" && v.layer && v.name !== undefined) return "F" + v.layer.id + ":" + v.name;
        return v;
    });
    if (!this.solvers[key]) this.solvers[key] = s;
    return this.solvers[key];
};

// The spec of one target: its formula and the nodes it needs.
RK.rkx.Builder.prototype.spec = function (tg) {
    var self = this, L = [], idx = {}, N = {}, Q = [], qs = [], B = [], bs = [];
    var li = function (layer) {
        var id = layer.id;
        if (idx[id] === undefined) {
            idx[id] = L.length;
            L.push([id, layer.name]);
        }
        return idx[id];
    };
    var fx = function (r) {
        if (!r) return null;
        return [li(r.layer), r.name, r.uid];
    };
    var need, node, conv;
    need = function (layer) {
        var i = li(layer);
        if (self.isNode(layer)) node(layer);
        return i;
    };
    node = function (layer) {
        var i = li(layer);
        if (N[i]) return i;
        var n = { p: -1 };
        N[i] = n;
        var vp = self.parentOf(layer);
        if (vp) n.p = need(vp);
        if (self.isFlat(layer)) n.ap = layer.parent ? need(layer.parent) : -1;
        var fs = self.formulas(layer), f = {}, any = false;
        for (var k in fs) {
            f[k] = conv(fs[k]);
            any = true;
        }
        if (any) n.f = f;
        var cst = RK.rkx.constants(layer, fs);
        if (cst) n.c = cst;
        return i;
    };
    var solver = function (q) {
        for (var j = 0; j < qs.length; j++) if (qs[j] === q) return j;
        var at = Q.length, s = {};
        qs.push(q);
        Q.push(s);
        s.b = q.b.map(need);
        s.c = need(q.c);
        s.g = q.g.map(fx);
        s.l = fx(q.l);
        s.A = RK.rkx.R6(q.A);
        s.E = RK.rkx.R6(q.E);
        return at;
    };
    var curve = function (z) {
        for (var j = 0; j < bs.length; j++) if (bs[j] === z) return j;
        var at = B.length, s = {};
        bs.push(z);
        B.push(s);
        s.e = need(z.e);
        s.r = need(z.r);
        s.k1 = need(z.k1);
        s.k2 = z.k2 ? need(z.k2) : -1;
        s.o = fx(z.o);
        s.a = fx(z.a);
        s.b = z.b.map(need);
        s.w = z.w.map(fx);
        s.t = RK.rkx.R6(z.t);
        s.p = RK.rkx.R6(z.p);
        return at;
    };
    conv = function (f) {
        switch (f.k) {
        case "look": return { k: "look", c: need(f.c), w: fx(f.w), v: fx(f.v), f: fx(f.f), x: f.x && f.x.length ? f.x.map(fx) : undefined };
        case "goal": return { k: "goal", c: f.c ? need(f.c) : -1, s: fx(f.s) };
        case "ik": return { k: "ik", q: solver(f.q), o: f.o, e: f.e ? RK.rkx.R6(f.e) : undefined };
        case "bz": return { k: "bz", b: curve(f.b), i: f.i, o: f.o };
        case "roll": return { k: "roll", m: f.m, t: fx(f.t), h: fx(f.h), r: fx(f.r) };
        case "apos": return { k: "apos", c: need(f.c), w: fx(f.w), l: RK.rkx.R6(f.l) };
        case "neck": return { k: "neck", s: need(f.s), o: fx(f.o), t: need(f.t), h: f.h ? need(f.h) : -1, t0: RK.rkx.R6(f.t0), n: f.n };
        case "lock": return f.v === undefined ? { k: "lock" } : { k: "lock", v: f.v };
        case "flat": return { k: "flat" };
        }
        throw new Error("unknown formula " + f.k);
    };
    var t = li(tg.layer);
    if (self.isNode(tg.layer)) node(tg.layer);
    var o = { l: t, k: tg.k, f: conv(tg.f) };
    // c: the comp it was written in (in a copy, layers renamed since are found through it)
    return { L: L, N: N, Q: Q, B: B, c: this.comp.id, o: o };
};

// What a formula's value depends on: the "layerId:k" keys of the formulas it computes or reads,
// all the way up (so the writing order can put them first).
RK.rkx.Builder.prototype.fdeps = function (layer, k, f) {
    var self = this, out = {}, seen = this.dmemo || (this.dmemo = {});
    var fk = function (L, kk) {
        if (!L) return;
        var fs = self.formulas(L);
        if (!fs[kk]) return;
        var key = L.id + ":" + kk;
        if (out[key]) return;
        out[key] = true;
        var sub = seen[key];
        if (!sub) {
            seen[key] = {};
            sub = seen[key] = self.fdeps(L, kk, fs[kk]);
        }
        for (var x in sub) out[x] = true;
    };
    var world = function (L) {
        if (!L) return;
        world(self.parentOf(L));
        fk(L, "p");
        fk(L, "r");
        fk(L, "s");
    };
    var pw = function (L) { if (L) world(self.parentOf(L)); };
    var aw = function (L) {
        if (!L) return;
        pw(L);
        fk(L, "p");
        if (!self.isNode(L)) {
            fk(L, "r");
            fk(L, "s");
        }
    };
    var i;
    switch (f.k) {
    case "look": pw(layer); fk(layer, "p"); aw(f.c); break;
    case "goal": pw(layer); if (f.c) { world(f.c); fk(f.c, "r"); } break;
    case "ik":
        var q = f.q;
        pw(q.b[0]);
        fk(q.b[0], "p");
        for (i = 0; i < q.b.length; i++) fk(q.b[i], "s");
        aw(q.c);
        break;
    case "bz":
        var z = f.b;
        aw(z.e);
        aw(z.r);
        aw(z.k1);
        aw(z.k2);
        if (f.o === "r") {
            aw(layer);
            if (f.i > 0) aw(z.b[f.i - 1]);
        }
        break;
    case "apos": pw(layer); aw(f.c); break;
    case "neck": fk(f.t, "r"); fk(f.h, "r"); fk(f.s, "r"); break;
    case "flat": world(layer); world(layer.parent); break;
    }
    return out;
};

// A hidden null Rig Kit made (zero, pivot, curve root): its scale and anchor point don't change,
// so the expressions take them as numbers instead of reading them (a zero's position and
// rotation too).
RK.rkx.constants = function (layer, fs) {
    try {
        if (!layer.nullLayer || layer.enabled !== false) return null;
        var out = null, tr = layer.transform;
        var sc = tr.scale, an = tr.anchorPoint;
        if (!fs.s && sc.numKeys === 0 && !sc.expression) {
            out = out || {};
            out.s = RK.rkx.R6([sc.value[0], sc.value[1]]);
        }
        if (an.numKeys === 0 && !an.expression) {
            out = out || {};
            out.a = RK.rkx.R6([an.value[0], an.value[1]]);
        }
        // a zero holds its layer's rest transform: its position and rotation don't change either
        if (String(layer.comment || "").indexOf(RK.ZERO_TAG) >= 0) {
            var po = tr.position, ro = RK.rotProp(layer);
            if (!fs.p && po.numKeys === 0 && !po.expression && !po.dimensionsSeparated) {
                out = out || {};
                out.p = RK.rkx.R6([po.value[0], po.value[1], po.value.length > 2 ? po.value[2] : 0]);
            }
            if (!fs.r && ro.numKeys === 0 && !ro.expression) {
                out = out || {};
                out.r = RK.rkx.R6(ro.value);
            }
        }
        return out;
    } catch (e) {
        return null;
    }
};

// Write every registered expression: upstream first, each with its no-jump offset; then
// flatten the limbs asked for; then bring the comp's other rig expressions up to date.
RK.rkx.Builder.prototype.commit = function () {
    var comp = this.comp, changed = {}, written = {}, i;
    for (i = 0; i < this.clears.length; i++) {
        (function (c) {
            RK.withUnlocked(c.layer, function () { c.prop.expression = ""; });
            changed[c.layer.id] = true;
        })(this.clears[i]);
    }
    this.clears = [];
    var T = this.targets;
    this.targets = [];
    this.write(T, changed, written);
    if (this.limbs.length) this.flattenNow(changed, written);
    RK.rkx.refresh(comp, changed, written);
};

RK.rkx.Builder.prototype.write = function (T, changed, written) {
    var t = this.comp.time, i;
    if (!T.length) return;
    this.memo = {};
    this.dmemo = {};
    var byKey = {};
    for (i = 0; i < T.length; i++) {
        var tg = T[i];
        tg.key = tg.layer.id + ":" + (tg.k === "x" ? "x" + tg.prop.__uid : tg.k);
        byKey[tg.key] = tg;
        tg.spec = this.spec(tg);
        // (a flattened layer's output reads virtual formulas and static values, never another
        // output: no order among them)
        var dd = tg.f.k === "flat" ? {} : this.fdeps(tg.layer, tg.k, tg.f);
        tg.deps = [];
        for (var key in dd) tg.deps.push(key);
        tg.v0 = tg.prop.numKeys ? null : tg.prop.valueAtTime(t, false);
    }
    var order = [], state = {};
    var visit = function (tg) {
        if (state[tg.key] === 2) return;
        if (state[tg.key] === 1) throw new Error("the rig reads itself in a circle (" + tg.layer.name + ")");
        state[tg.key] = 1;
        for (var j = 0; j < tg.deps.length; j++) {
            var d = byKey[tg.deps[j]];
            if (d && d !== tg) visit(d);
        }
        state[tg.key] = 2;
        order.push(tg);
    };
    for (i = 0; i < T.length; i++) visit(T[i]);
    for (i = 0; i < order.length; i++) {
        (function (tg) {
            changed[tg.layer.id] = true;
            written[tg.key] = true;
            RK.withUnlocked(tg.layer, function () {
                var p = tg.prop;
                if (tg.f.k === "ik" && tg.k === "r" && p.numKeys === 0) p.setValue(0);
                RK.setExpr(p, RK.rkx.text(tg.label, tg.spec));
                if (RK.rkx.ADDITIVE[tg.f.k]) RK.noJump(p, tg.v0 === null ? p.keyValue(1) : tg.v0);
            });
        })(order[i]);
    }
};

// The limbs asked for (RK.rkx.Builder.flatten), once their formulas are written: each layer is
// parented to the limb's frame without moving its static values (they stay relative to its
// virtual parent), and its position and rotation show what the chain gives it.
RK.rkx.Builder.prototype.flattenNow = function (changed, written) {
    var comp = this.comp, T = [], i, j;
    this.memo = {};
    for (i = 0; i < this.limbs.length; i++) {
        var limb = this.limbs[i];
        // the frame: the first layer above the limb that no rig formula moves
        var frame = this.flats[limb[0].id].vp;
        while (frame && this.isNode(frame)) frame = this.parentOf(frame);
        for (j = 0; j < limb.length; j++) {
            var L = limb[j], f = this.flats[L.id];
            if ((f.vp ? f.vp.id : 0) === (frame ? frame.id : 0)) {
                delete this.flats[L.id];
                continue;
            }
            (function (L) {
                RK.withUnlocked(L, function () {
                    app.run("layer.setParent", { comp: comp.id, layers: [L.id], parent: frame ? frame.id : null, compensate: false });
                });
            })(L);
            f.done = true;
            T.push({ layer: L, k: "p", prop: L.transform.position, f: { k: "flat" }, label: "Auto-Rig " + L.name + " position" });
            T.push({ layer: L, k: "r", prop: RK.rotProp(L), f: { k: "flat" }, label: "Auto-Rig " + L.name + " rotation" });
        }
    }
    this.limbs = [];
    this.memo = {};
    this.dmemo = {};
    this.write(T, changed, written);
};

// Rewrite the comp's other rig expressions that compute a changed layer (their copy of its
// formulas is out of date). changed: {layer id: true}; skip: {"id:k": true} just written.
RK.rkx.refresh = function (comp, changed, skip) {
    var b = new RK.rkx.Builder(comp), n = 0;
    for (var i = 1; i <= comp.numLayers; i++) {
        var L = comp.layer(i);
        var props = { p: L.transform.position, r: RK.rotProp(L), s: L.transform.scale };
        for (var k in props) {
            var p = props[k];
            if (!p || skip[L.id + ":" + k]) continue;
            var text = "";
            try {
                text = p.expression;
            } catch (e) {
                continue;
            }
            var S = RK.rkx.parse(text);
            if (!S || !S.o || S.o.k !== k) continue;
            var hit = false;
            for (var j = 0; j < S.L.length && !hit; j++) if (changed[S.L[j][0]] && S.L[j][0] !== L.id) hit = true;
            if (!hit) continue;
            var f = b.unpack(S, S.o.f);
            if (!f) continue;
            var spec = b.spec({ layer: L, k: k, f: f });
            var label = text.substring(RK.TAG.length, text.indexOf("\n"));
            var nt = RK.rkx.text(label, spec);
            if (nt === text) continue;
            (function (L, p, nt) {
                RK.withUnlocked(L, function () {
                    var on = p.expressionEnabled;
                    p.expression = nt;
                    if (on === false) p.expressionEnabled = false;
                });
            })(L, p, nt);
            n++;
        }
    }
    return n;
};

// Run fn(builder) and write what it registered; nested calls share the outer builder (the
// Auto-Rig writes a whole character at once).
RK.rkx.cur = null;
RK.rkx.with = function (comp, fn) {
    var cur = RK.rkx.cur;
    if (cur && cur.comp.id === comp.id) return fn(cur);
    var b = new RK.rkx.Builder(comp);
    RK.rkx.cur = b;
    var r;
    try {
        r = fn(b);
    } finally {
        RK.rkx.cur = cur;
    }
    b.commit();
    return r;
};

// ============================================================================ controllers
//
// Controllers are the layers the animator keys. Two kinds (Settings > Rigging > Controllers):
//  - "icon": a guide shape layer whose content has one top-level group "Icon", holding one
//    sub-group per transform the controller is meant for ("Rotation", "xPosition", "yPosition",
//    "Scale", or a special shape "Arc", "Eye", "Camera"). The flags are read back from these
//    group names, the size from the Icon group's scale, the colour from its first stroke/fill.
//  - "null": a null object named "C_..." (all flags off).
// Name: "C_" + the layer it was made for. Comment tag: "|Controllers| ".

RK.ctrl = {};
RK.CTRL_TAG = "|Controllers| ";
RK.BONE_TAG = "|Bones| ";
RK.ZERO_TAG = "|Zeroes| ";
RK.ICON_UNITS = 30;   // an icon drawing is about 30 units across before scaling

RK.addTag = function (layer, tag) {
    try {
        var c = layer.comment || "";
        if (c.indexOf(tag) < 0) layer.comment = c + tag;
    } catch (e) {}
};

// "C_" + name, shortened as "C_" + first 10 + " ... " + last 13 when the name is long.
RK.ctrl.nameFor = function (layerName) {
    if (layerName === undefined || layerName === null) return "C_";
    var n = String(layerName);
    if (n.length > 29) n = n.substr(0, 10) + " ... " + n.substr(n.length - 13);
    return "C_" + n;
};

// Settings shared by every controller.
RK.ctrl.settings = function () {
    return {
        type: RK.pref("ctrlType", "icon"),           // "icon" | "null"
        placement: RK.pref("ctrlPlacement", "top"),  // "top" | "bottom" | "over" | "under"
        sizeHint: RK.pref("ctrlSizeHint", "medium"), // "small" | "medium" | "big"
        autoSize: RK.pref("ctrlAutoSize", true),
        size: RK.pref("ctrlSize", 100),               // px, when not automatic
        color: RK.pref("ctrlColor", [1, 1, 1])
    };
};

// Controller size in pixels for a comp.
RK.ctrl.sizePx = function (comp, st) {
    st = st || RK.ctrl.settings();
    if (!st.autoSize) return Math.max(1, st.size);
    var S = comp.width / 2 + comp.height / 2;
    var d = st.sizeHint === "small" ? 40 : st.sizeHint === "big" ? 10 : 20;
    return Math.max(4, Math.floor(S / d));
};

// ---------------------------------------------------------------------------- icon drawings
//
// Units: a box about 30 across centred on the layer origin, y down. Each part is a list of
// paths {v, i, o, closed} (see RK.circlePath / RK.polyPath) plus whether it is filled.

RK.ctrl.parts = {
    Rotation: function () {
        var pts = [], n = 12, a0 = 140, a1 = 400;   // an open circle with a gap at the top left
        for (var i = 0; i <= n; i++) {
            var a = (a0 + (a1 - a0) * i / n) * Math.PI / 180;
            pts.push([15 * Math.cos(a), 15 * Math.sin(a)]);
        }
        var e = pts[pts.length - 1];
        return { paths: [RK.ctrl.smooth(pts, false), RK.polyPath([[e[0] - 7, e[1] - 4], [e[0], e[1]], [e[0] + 2, e[1] - 8]], false)] };
    },
    Arc: function () {
        var pts = [], n = 6, a0 = 230, a1 = 310;
        for (var i = 0; i <= n; i++) {
            var a = (a0 + (a1 - a0) * i / n) * Math.PI / 180;
            pts.push([18 * Math.cos(a), 18 * Math.sin(a) + 10]);
        }
        var e = pts[pts.length - 1];
        return { paths: [RK.ctrl.smooth(pts, false), RK.polyPath([[e[0] - 7, e[1] - 3], [e[0], e[1]], [e[0] - 2, e[1] + 7]], false)] };
    },
    xPosition: function () {
        return { paths: [RK.polyPath([[-15, 0], [15, 0]], false), RK.polyPath([[9, -6], [15, 0], [9, 6]], false), RK.polyPath([[-9, -6], [-15, 0], [-9, 6]], false)] };
    },
    yPosition: function () {
        return { paths: [RK.polyPath([[0, -15], [0, 15]], false), RK.polyPath([[-6, 9], [0, 15], [6, 9]], false), RK.polyPath([[-6, -9], [0, -15], [6, -9]], false)] };
    },
    Scale: function () {
        return { paths: [RK.polyPath([[-19, -19], [19, -19], [19, 19], [-19, 19]], true), RK.polyPath([[11, 11], [19, 19]], false), RK.polyPath([[13, 19], [19, 19], [19, 13]], false)] };
    },
    Eye: function () {
        var k = 9;
        return {
            paths: [
                { v: [[-14, 0], [14, 0]], i: [[0, 0], [k, -10]], o: [[-k, -10], [0, 0]], closed: false },
                { v: [[-14, 0], [14, 0]], i: [[0, 0], [k, 10]], o: [[-k, 10], [0, 0]], closed: false },
                RK.circlePath(4.5)
            ],
            fillLast: true
        };
    },
    Camera: function () {
        return {
            paths: [
                RK.polyPath([[-15, -6], [5, -6], [5, -2], [15, -7], [15, 7], [5, 2], [5, 6], [-15, 6]], true),
                RK.circlePath(4.5, -9, -11), RK.circlePath(3.5, 0, -10)
            ],
            fill: true
        };
    }
};

// A polyline through points with smooth (Catmull-Rom style) tangents.
RK.ctrl.smooth = function (pts, closed) {
    var n = pts.length, ins = [], outs = [];
    for (var i = 0; i < n; i++) {
        var a = pts[Math.max(0, i - 1)], b = pts[Math.min(n - 1, i + 1)];
        var tx = (b[0] - a[0]) / 6, ty = (b[1] - a[1]) / 6;
        if (i === 0 || i === n - 1) { tx = 0; ty = 0; }
        ins.push([-tx, -ty]);
        outs.push([tx, ty]);
    }
    return { v: pts, i: ins, o: outs, closed: !!closed };
};

// Flags -> which parts are drawn. Arc, Eye and Camera replace the others.
RK.ctrl.partsFor = function (f) {
    if (f.camera) return ["Camera"];
    if (f.eye) return ["Eye", "xPosition", "yPosition"];
    if (f.arc) return ["Arc"];
    var out = [];
    if (f.rotation) out.push("Rotation");
    if (f.x) out.push("xPosition");
    if (f.y) out.push("yPosition");
    if (f.scale) out.push("Scale");
    if (!out.length) out.push("Rotation");
    return out;
};

// Draw (or redraw) the Icon group of a shape-layer controller.
RK.ctrl.draw = function (layer, flags, sizePx, color) {
    var root = layer.property("ADBE Root Vectors Group");
    for (var i = root.numProperties; i >= 1; i--) if (root.property(i).name === "Icon") root.property(i).remove();
    var icon = root.addProperty("ADBE Vector Group");
    icon.name = "Icon";
    var items = icon.property("ADBE Vectors Group");
    var parts = RK.ctrl.partsFor(flags);
    var both = flags.rotation && !flags.arc && !flags.eye && !flags.camera;
    for (var p = 0; p < parts.length; p++) {
        var name = parts[p];
        var def = RK.ctrl.parts[name]();
        var g = items.addProperty("ADBE Vector Group");
        g.name = name;
        var gi = g.property("ADBE Vectors Group");
        for (var k = 0; k < def.paths.length; k++) {
            if (name === "Eye" && k === def.paths.length - 1) continue;
            RK.ctrl.addPath(gi, def.paths[k]);
        }
        var st = gi.addProperty("ADBE Vector Graphic - Stroke");
        st.property("ADBE Vector Stroke Color").setValue([color[0], color[1], color[2]]);
        st.property("ADBE Vector Stroke Width").setValue(2);
        if (def.fill) {
            var fl = gi.addProperty("ADBE Vector Graphic - Fill");
            fl.property("ADBE Vector Fill Color").setValue([color[0], color[1], color[2]]);
        }
        if (name === "Eye") {
            // the pupil, filled, in its own group so the lids stay unfilled
            var pg = gi.addProperty("ADBE Vector Group");
            pg.name = "Pupil";
            var pgi = pg.property("ADBE Vectors Group");
            RK.ctrl.addPath(pgi, def.paths[def.paths.length - 1]);
            var pf = pgi.addProperty("ADBE Vector Graphic - Fill");
            pf.property("ADBE Vector Fill Color").setValue([color[0], color[1], color[2]]);
        }
        if (both && (name === "xPosition" || name === "yPosition")) g.property("ADBE Vector Transform Group").property("ADBE Vector Scale").setValue([70, 70]);
    }
    var s = sizePx * 100 / RK.ICON_UNITS;
    icon.property("ADBE Vector Transform Group").property("ADBE Vector Scale").setValue([s, s]);
    return icon;
};

RK.ctrl.addPath = function (items, p) {
    var sh = new Shape();
    sh.vertices = p.v;
    sh.inTangents = p.i;
    sh.outTangents = p.o;
    sh.closed = p.closed !== false;
    var pp = items.addProperty("ADBE Vector Shape - Group");
    pp.property("ADBE Vector Shape").setValue(sh);
    return pp;
};

// ---------------------------------------------------------------------------- recognising

RK.ctrl.iconGroup = function (layer) {
    if (!(layer instanceof ShapeLayer)) return null;
    var root = layer.property("ADBE Root Vectors Group");
    if (!root) return null;
    for (var i = 1; i <= root.numProperties; i++) if (root.property(i).name === "Icon") return root.property(i);
    return null;
};

RK.ctrl.isController = function (layer) {
    if (layer instanceof ShapeLayer) return !!RK.ctrl.iconGroup(layer);
    if (layer instanceof AVLayer) return String(layer.name).indexOf("C_") === 0;
    return false;
};

// What a controller is: {kind: "icon"|"null", flags, size (px), color}.
RK.ctrl.read = function (layer) {
    var icon = RK.ctrl.iconGroup(layer);
    var f = { rotation: false, x: false, y: false, scale: false, arc: false, eye: false, camera: false };
    if (!icon) return { kind: "null", flags: f, size: 100, color: [1, 1, 1] };
    var items = icon.property("ADBE Vectors Group"), color = null;
    var map = { rotation: "rotation", xposition: "x", yposition: "y", scale: "scale", arc: "arc", eye: "eye", camera: "camera" };
    for (var i = 1; i <= items.numProperties; i++) {
        var g = items.property(i), k = map[String(g.name).toLowerCase()];
        if (k) f[k] = true;
        if (!color && g.property("ADBE Vectors Group")) {
            var gi = g.property("ADBE Vectors Group");
            for (var j = 1; j <= gi.numProperties && !color; j++) {
                var it = gi.property(j);
                if (it.matchName === "ADBE Vector Graphic - Stroke") color = it.property("ADBE Vector Stroke Color").value;
                else if (it.matchName === "ADBE Vector Graphic - Fill") color = it.property("ADBE Vector Fill Color").value;
            }
        }
    }
    if (f.eye) { f.x = true; f.y = true; }
    var sc = icon.property("ADBE Vector Transform Group").property("ADBE Vector Scale").value;
    return { kind: "icon", flags: f, size: Math.round(sc[0] * RK.ICON_UNITS / 100), color: color ? [color[0], color[1], color[2]] : [1, 1, 1] };
};

// ---------------------------------------------------------------------------- creation

// Create a controller for `layer` (or at the comp centre without one).
// opts: {flags: {rotation, x, y, scale, arc, eye, camera}, lock: bool, name, type, color, size,
//        select: bool (default true), position: [x, y]}
RK.ctrl.create = function (comp, layer, opts) {
    opts = opts || {};
    var st = RK.ctrl.settings();
    var type = opts.type || st.type;
    var f = opts.flags || { rotation: true, x: true, y: true, scale: false };
    var name = opts.name || RK.ctrl.nameFor(layer ? layer.name : null);
    var pos = opts.position || (layer ? RK.compPoint(layer) : [comp.width / 2, comp.height / 2]);
    var size = opts.size || RK.ctrl.sizePx(comp, st);
    var color = opts.color || st.color;
    var c;
    if (type === "null") {
        c = comp.layers.addNull();
        c.name = name;
    } else {
        c = comp.layers.addShape();
        c.name = name;
        RK.ctrl.draw(c, f, size, color);
        c.guideLayer = true;
    }
    c.transform.position.setValue([pos[0], pos[1], 0]);
    RK.addTag(c, RK.CTRL_TAG);
    RK.ctrl.place(c, layer, opts.placement || st.placement);
    if (opts.lock) RK.ctrl.lock(c, f);
    if (opts.select !== false) RK.selectOnly(comp, [c]);
    return c;
};

RK.ctrl.place = function (c, ref, placement) {
    try {
        if (placement === "bottom") c.moveToEnd();
        else if (ref && placement === "over") c.moveBefore(ref);
        else if (ref && placement === "under") c.moveAfter(ref);
    } catch (e) {}
};

// Constant expressions on the transforms the controller is not meant to change (its flags,
// or the flags given).
RK.ctrl.lock = function (c, flags) {
    var info = RK.ctrl.read(c), f = flags || info.flags;
    if (!flags && info.kind === "null") f = { rotation: false, x: false, y: false, scale: false };
    var tr = c.transform;
    var keep = function (prop, v) {
        RK.setExpr(prop, RK.TAG + "lock\n" + RK.xValue(v));
    };
    if (!f.eye && !f.camera && !(f.x && f.y)) {
        var pos = tr.position;
        if (pos.dimensionsSeparated) {
            var px = tr.property("ADBE Position_0"), py = tr.property("ADBE Position_1");
            if (!f.x) keep(px, px.value);
            if (!f.y) keep(py, py.value);
        } else {
            var v = pos.value;
            if (!f.x && !f.y) RK.setExpr(pos, RK.TAG + "lock\n" + RK.xValue([v[0], v[1]]));
            else if (f.x) RK.setExpr(pos, RK.TAG + "lock\n[value[0], " + RK.num(v[1]) + "]");
            else RK.setExpr(pos, RK.TAG + "lock\n[" + RK.num(v[0]) + ", value[1]]");
        }
    }
    if (!f.rotation && !f.arc && !f.camera) keep(tr.rotation, tr.rotation.value);
    if (!f.scale) {
        var s = tr.scale.value;
        keep(tr.scale, [s[0], s[1]]);
    }
};

// Remove the expressions on Position, Rotation and Scale, keeping what they showed.
RK.ctrl.unlock = function (c) {
    var tr = c.transform;
    var props = [tr.rotation, tr.scale];
    if (tr.position.dimensionsSeparated) props.push(tr.property("ADBE Position_0"), tr.property("ADBE Position_1"), tr.property("ADBE Position_2"));
    else props.push(tr.position);
    for (var i = 0; i < props.length; i++) {
        var p = props[i];
        if (!p || !p.expression) continue;
        var v = p.value;
        p.expression = "";
        if (p.numKeys === 0) {
            try {
                p.setValue(v);
            } catch (e) {}
        }
    }
};

// ---------------------------------------------------------------------------- small helpers

RK.num = function (v) {
    var r = Math.round(v * 10000) / 10000;
    return String(r === 0 ? 0 : r);
};
RK.xValue = function (v) {
    if (v instanceof Array) {
        var o = [];
        for (var i = 0; i < v.length; i++) o.push(RK.num(v[i]));
        return "[" + o.join(", ") + "]";
    }
    return RK.num(v);
};

RK.selectOnly = function (comp, layers) {
    try {
        app.run("layer.select", { comp: comp.id, layers: layers.map(function (l) { return l.id; }) });
        return;
    } catch (e) {}
    var sel = comp.selectedLayers;
    for (var i = 0; i < sel.length; i++) sel[i].selected = false;
    for (var j = 0; j < layers.length; j++) layers[j].selected = true;
};

// ============================================================================ rigging core
//
// Shared by every tab: world transforms, the name check, effect groups (several expression
// controls standing in for one custom effect), "rig a property", zeros and helper-layer flags.

// ---------------------------------------------------------------------------- world transforms

// A layer's anchor point in the comp and its world rotation and scale, now.
// {pos: [x, y, z], rot: degrees, sx, sy: world scale factors (1 = 100 %)}
RK.world = function (layer, t) {
    var comp = layer.containingComp;
    if (t === undefined) t = comp.time;
    var a = layer.transform.anchorPoint ? layer.transform.anchorPoint.valueAtTime(t, false) : [0, 0, 0];
    if (layer instanceof AVLayer && !RK.is3D(layer) && !RK.hasParent3D(layer) && Math.abs(t - comp.time) < 1e-9) {
        try {
            var p0 = layer.sourcePointToComp([a[0], a[1]]);
            var px = layer.sourcePointToComp([a[0] + 100, a[1]]);
            var py = layer.sourcePointToComp([a[0], a[1] + 100]);
            var vx = [px[0] - p0[0], px[1] - p0[1]], vy = [py[0] - p0[0], py[1] - p0[1]];
            return {
                pos: [p0[0], p0[1], 0],
                rot: Math.atan2(vx[1], vx[0]) * 180 / Math.PI,
                sx: Math.sqrt(vx[0] * vx[0] + vx[1] * vx[1]) / 100,
                sy: Math.sqrt(vy[0] * vy[0] + vy[1] * vy[1]) / 100
            };
        } catch (e) {}
    }
    // (cameras and lights have no anchor point: their origin)
    var r = RK.probe(layer, "var a = L.anchorPoint ? L.anchorPoint.valueAtTime(T) : [0, 0, 0];\n" +
        "var p = L.toWorld(a, T), x = L.toWorldVec([100, 0, 0], T), y = L.toWorldVec([0, 100, 0], T);\n" +
        "[p[0], p[1], p.length > 2 ? p[2] : 0, Math.atan2(x[1], x[0]) * 180 / Math.PI]", t);
    var r2 = RK.probe(layer, "var x = L.toWorldVec([100, 0, 0], T), y = L.toWorldVec([0, 100, 0], T);\n[length(x) / 100, length(y) / 100, 0, 0]", t);
    return { pos: [r[0], r[1], r[2]], rot: r[3], sx: r2[0], sy: r2[1] };
};

RK.hasParent3D = function (layer) {
    var p = layer.parent, n = 0;
    while (p && n++ < 100) {
        if (RK.is3D(p)) return true;
        p = p.parent;
    }
    return false;
};

// Evaluate an expression returning up to four numbers with L = the layer and T = the time,
// through a Color Control on a temporary null (its four values aren't clamped).
// The user's layer selection is put back afterwards (a new layer is selected when made).
RK.probe = function (layer, code, t) {
    var comp = layer.containingComp;
    if (t === undefined) t = comp.time;
    var sel = RK.selectionIds(comp), props = [];
    try {
        props = comp.selectedProperties;
    } catch (e) {}
    var tmp = comp.layers.addNull();
    try {
        var fx = RK.addEffect(tmp, "ADBE Color Control", "rk probe");
        fx.property(1).expression = "var L = thisComp.layer(" + layer.index + ");\nvar T = " + t + ";\n" + code;
        var v = fx.property(1).valueAtTime(t, false);
        return [v[0], v[1], v[2], v[3]];
    } finally {
        tmp.remove();
        RK.restoreSelection(comp, sel);
        // re-selecting layers clears the property selection: give it back
        for (var i = 0; i < props.length; i++) {
            try {
                props[i].selected = true;
            } catch (e) {}
        }
    }
};

RK.selectionIds = function (comp) {
    try {
        return app.run("comp.info", { comp: comp.id }).selectedLayers || comp.selectedLayers.map(function (l) { return l.id; });
    } catch (e) {
        return comp.selectedLayers.map(function (l) { return l.id; });
    }
};
RK.restoreSelection = function (comp, ids) {
    try {
        app.run("layer.select", { comp: comp.id, layers: ids });
    } catch (e) {}
};

// The sign of a layer's world transform (-1 when a negative scale mirrors it), without
// touching the project when the layer is 2D.
RK.mirrorSign = function (layer) {
    if (layer instanceof AVLayer && !RK.is3D(layer) && !RK.hasParent3D(layer)) {
        try {
            var a = layer.sourcePointToComp([0, 0]), x = layer.sourcePointToComp([100, 0]), y = layer.sourcePointToComp([0, 100]);
            var det = (x[0] - a[0]) * (y[1] - a[1]) - (x[1] - a[1]) * (y[0] - a[0]);
            return det < 0 ? -1 : 1;
        } catch (e) {}
    }
    var r = RK.probe(layer, "var x = L.toWorldVec([1, 0, 0], T), y = L.toWorldVec([0, 1, 0], T);\n[x[0] * y[1] - x[1] * y[0], 0, 0, 0]");
    return r[0] < 0 ? -1 : 1;
};

RK.dist = function (a, b) {
    var dx = a[0] - b[0], dy = a[1] - b[1], dz = (a[2] || 0) - (b[2] || 0);
    return Math.sqrt(dx * dx + dy * dy + dz * dz);
};

RK.angleOf = function (v) {
    return Math.atan2(v[1], v[0]) * 180 / Math.PI;
};

// The rotation property a rig drives: Rotation, or Z Rotation on 3D layers (the same property).
RK.rotProp = function (layer) {
    return layer.transform.property("ADBE Rotate Z");
};

// ---------------------------------------------------------------------------- names

// Make every layer name in the comp unique: the n-th repeat of a name becomes "name n".
// Expressions aren't updated (like the original's name check); returns the number renamed.
RK.nameCheck = function (comp) {
    var info;
    try {
        info = app.run("comp.info", { comp: comp.id }).layers;
    } catch (e) {
        info = null;
    }
    var seen = {}, all = {}, n = 0, i, list = [];
    if (info) {
        info.sort(function (a, b) { return a.index - b.index; });
        for (i = 0; i < info.length; i++) list.push({ id: info[i].id, name: info[i].name, index: info[i].index });
    } else {
        for (i = 1; i <= comp.numLayers; i++) list.push({ id: comp.layer(i).id, name: comp.layer(i).name, index: i });
    }
    for (i = 0; i < list.length; i++) all[list[i].name] = true;
    for (i = 0; i < list.length; i++) {
        var nm = list[i].name;
        seen[nm] = (seen[nm] || 0) + 1;
        if (seen[nm] < 2) continue;
        var k = seen[nm], nn = nm + " " + k;
        while (all[nn]) nn = nm + " " + (++k);
        all[nn] = true;
        var L = comp.layer(list[i].index);
        var locked = L.locked;
        if (locked) L.locked = false;
        L.name = nn;
        if (locked) L.locked = true;
        n++;
    }
    return n;
};

// "base", then "base_2", "base_3"... among the effects of the layer.
RK.uniqueEffectName = function (layer, base) {
    var fx = RK.effects(layer), names = {};
    for (var i = 1; i <= fx.numProperties; i++) names[fx.property(i).name] = true;
    if (!RK.fxgTaken(names, base)) return base;
    var k = 2;
    while (RK.fxgTaken(names, base + "_" + k)) k++;
    return base + "_" + k;
};
RK.fxgTaken = function (names, base) {
    if (names[base]) return true;
    for (var n in names) if (n.indexOf(base + RK.SEP) === 0) return true;
    return false;
};

// Last characters of a name (for "Zero_" + last 24 characters and the like).
RK.tail = function (s, n) {
    s = String(s);
    return s.length > n ? s.substr(s.length - n) : s;
};

// ---------------------------------------------------------------------------- effect groups
//
// EffectCraft scripts can't define new effects, so a custom effect with several parameters is
// a run of expression controls named "<group>: <parameter>". Expressions read parameter p of
// group g as effect("g: p")(1). Their fx switch is off on layers that render (the values still
// read and animate; an effect that is on costs render time on every frame even if it draws
// nothing).

RK.SEP = ": ";

RK.fxg = function (group, param) {
    return param ? group + RK.SEP + param : group;
};

// params: [{name, type: slider|angle|checkbox|point|point3d|color|layer|dropdown, value, items}]
// Returns {group, layer, fx: {param name: effect}}.
RK.addGroup = function (layer, group, params) {
    var out = { group: group, layer: layer, fx: {} };
    var mn = { slider: "ADBE Slider Control", angle: "ADBE Angle Control", checkbox: "ADBE Checkbox Control",
        point: "ADBE Point Control", point3d: "ADBE Point3D Control", color: "ADBE Color Control",
        layer: "ADBE Layer Control", dropdown: "ADBE Dropdown Control" };
    for (var i = 0; i < params.length; i++) {
        var p = params[i];
        var name = params.length === 1 && p.single ? group : RK.fxg(group, p.name);
        var e = RK.addEffect(layer, mn[p.type], name);
        if (p.type === "dropdown" && p.items) {
            app.run("effect.editDropdown", { comp: layer.containingComp.id, layer: layer.id, prop: e.property(1).__uid, items: p.items });
        }
        if (p.value !== undefined && p.value !== null) {
            var v = p.value;
            if (p.type === "checkbox") v = v ? 1 : 0;
            if (p.type === "layer") v = v && v.index ? v.index : 0;
            if (!(p.type === "layer" && !v)) e.property(1).setValue(v);
        }
        RK.fxOff(layer, e);
        out.fx[p.name] = e;
    }
    return out;
};

// Switch an expression control off unless its layer doesn't render (nulls, guides that are
// nulls); the value still reads.
RK.fxOff = function (layer, e) {
    try {
        if (!layer.nullLayer) e.enabled = false;
    } catch (err) {}
};

// Expression text reading a group parameter on layer variable `v` (or thisLayer if null).
RK.xfx = function (v, group, param) {
    return (v ? v + "." : "") + "effect(" + RK.q(RK.fxg(group, param)) + ")(1)";
};

// ---------------------------------------------------------------------------- properties

RK.canExpr = function (p) {
    try {
        return p instanceof Property && p.canSetExpression;
    } catch (e) {
        return false;
    }
};

// "The selected property": the last selected property of the first selected layer.
// Shows the standard messages and returns null when there is none.
RK.selectedProperty = function (comp) {
    var layers = comp.selectedLayers;
    if (!layers.length) {
        RK.msg("No layer is selected. Select a layer or a property.");
        return null;
    }
    var props = layers[0].selectedProperties;
    if (!props.length) {
        RK.msg("No property is selected. Select a property.");
        return null;
    }
    var p = props[props.length - 1];
    if (!RK.canExpr(p)) {
        RK.msg("The selected property (" + p.name + ") can't hold an expression, so it can't be rigged.");
        return null;
    }
    return p;
};

// 1, 2 or 3: Position and Scale count as 2D on 2D layers; colours count as 4 values.
RK.dims = function (p) {
    var t = p.propertyValueType;
    if (t === PropertyValueType.COLOR) return 4;
    if (t === PropertyValueType.ThreeD || t === PropertyValueType.ThreeD_SPATIAL) {
        var L = p.__layer, mn = p.matchName;
        if ((mn === "ADBE Position" || mn === "ADBE Scale" || mn === "ADBE Anchor Point") && !RK.is3D(L)) return 2;
        return 3;
    }
    if (t === PropertyValueType.TwoD || t === PropertyValueType.TwoD_SPATIAL) return 2;
    return 1;
};

// Re-find a property after effects were added to its layer (by its path of indices).
RK.propPath = function (p) {
    var path = [], q = p;
    while (q && !(q instanceof Layer)) {
        path.unshift(q.propertyIndex);
        q = q.parentProperty;
    }
    return path;
};
RK.propAt = function (layer, path) {
    var q = layer;
    for (var i = 0; i < path.length; i++) q = q.property(path[i]);
    return q;
};

// "No jump": the expression adds a constant offset; set the static value so the result is v0.
// Keyframed properties get every key shifted by the offset instead.
RK.noJump = function (prop, v0) {
    var comp = prop.__layer.containingComp, t = comp.time;
    if (prop.numKeys > 0) {
        var t0 = prop.keyTime(1);
        var v1 = prop.valueAtTime(t0, false);
        var k0 = prop.keyValue(1);
        var off = RK.vsub(k0, v1);
        for (var i = 1; i <= prop.numKeys; i++) prop.setValueAtKey(i, RK.vadd(prop.keyValue(i), off));
        return;
    }
    // static' = static + (v0 - shown): right whatever the property showed before (its static
    // value, or another expression's result)
    var v = prop.valueAtTime(t, false), s = null;
    try {
        s = prop.valueAtTime(t, true);
    } catch (e) {}
    if (s === null || s === undefined) s = v0;
    prop.setValue(RK.vadd(s, RK.vsub(v0, v)));
};

RK.vadd = function (a, b) {
    if (!(a instanceof Array)) return a + b;
    var o = [];
    for (var i = 0; i < a.length; i++) o.push(a[i] + (b[i] || 0));
    return o;
};
RK.vsub = function (a, b) {
    if (!(a instanceof Array)) return a - b;
    var o = [];
    for (var i = 0; i < a.length; i++) o.push(a[i] - (b[i] || 0));
    return o;
};
RK.vmul = function (a, k) {
    if (!(a instanceof Array)) return a * k;
    var o = [];
    for (var i = 0; i < a.length; i++) o.push(a[i] * k);
    return o;
};

// ---------------------------------------------------------------------------- helper layers

// Hide a helper: opts {guide, shy, locked, off (video off), bottom}
RK.hide = function (layer, opts) {
    if (opts.bottom) {
        try {
            layer.moveToEnd();
        } catch (e) {}
    }
    if (opts.guide) try { layer.guideLayer = true; } catch (e) {}
    if (opts.shy) layer.shy = true;
    if (opts.off) layer.enabled = false;
    if (opts.locked) layer.locked = true;
};

RK.withUnlocked = function (layer, fn) {
    var l = layer.locked;
    if (l) layer.locked = false;
    try {
        return fn();
    } finally {
        if (l) layer.locked = true;
    }
};

// ---------------------------------------------------------------------------- zeros

// Insert a null between a layer and its parent so the layer's transform reads zero.
// Returns the zero. opts: {name}
RK.zero = function (layer, opts) {
    opts = opts || {};
    var comp = layer.containingComp;
    var w = RK.world(layer);
    var oldParent = layer.parent;
    var z = comp.layers.addNull();
    z.name = opts.name || "Zero_" + RK.tail(layer.name, 24);
    RK.addTag(z, RK.ZERO_TAG);
    z.transform.anchorPoint.setValue([0, 0, 0]);
    z.transform.position.setValue([w.pos[0], w.pos[1], 0]);
    RK.rotProp(z).setValue(w.rot);
    // The zero takes the layer's world scale (the layer then reads 100 %), unless the layer's
    // scale is animated: then both keep theirs.
    var sc = layer.transform.scale;
    var scaleIt = sc.numKeys === 0 && !sc.expression;
    if (scaleIt) z.transform.scale.setValue([w.sx * 100, w.sy * 100, 100]);
    RK.withUnlocked(layer, function () {
        layer.parent = z;
        if (scaleIt) sc.setValue([100, 100, sc.value.length > 2 ? sc.value[2] : 100]);
        if (oldParent) z.parent = oldParent;
    });
    RK.hide(z, { bottom: true, guide: true, shy: true, off: true, locked: !opts.unlocked });
    return z;
};

// ---------------------------------------------------------------------------- effect space
//
// Effect points (Point Controls, puppet pins, effect points) are in the layer's effect space.
// For a layer with a source (footage, solid, precomp, null) that is its own layer space. A
// shape or text layer has no source size: its effect space is a comp-sized box centred on the
// layer's origin, so effect point p is layer point p - [compWidth / 2, compHeight / 2].
RK.effectOffset = function (layer) {
    if (layer instanceof ShapeLayer || layer instanceof TextLayer) {
        var c = layer.containingComp;
        return [c.width / 2, c.height / 2];
    }
    return [0, 0];
};

// ============================================================================ Rigging tab: IK
//
// The lead's module. IK (1, 2 and 3 bones, with or without a goal), the IK page, and the tab.
// IK Goal, Bezier IK, Controllers and Zero are in 51_rigmore.js; Auto-Rig in 55_autorig.js;
// the other rigging tools in 52_rigtools.js.
//
// How the IK expressions are written (EffectCraft): every property an IK drives (bone rotations,
// the stretched positions, the tip, the goal) solves the whole chain itself, from plain inputs
// only: the controller's world position (and rotation, for a goal), where the root sits (its
// parent's transform and its own position), the bones' scales, the IK parameters on the
// controller, and the rest geometry recorded at creation. No IK expression reads a value
// another IK expression computes, so a frame never evaluates an IK twice inside another.

RK.rig = {};
RK.ik = {};

RK.ui.addTab("rigging", "Rigging", RK.I.tabRig, "Rigging", function (page) {
    RK.ui.buildGrid(page, {
        top: ["autorig"],
        left: ["ik", "bezierik", "controllers", "zero", "rename", "replace"],
        right: ["ikgoal", "rotmorph", "bones", "list", "measure", "lock"]
    });
});

RK.undoName = function (tool) {
    return RK.NAME + " - " + tool;
};

// ---------------------------------------------------------------------------- analysis

// Work out which selected layers are bones, goal and controller (spec 10 §1.3).
// Returns {kind: "1"|"1+1"|"2"|"2+1"|"3"|"3+1", chain: [root..end(, goal)], ctrl, clockwise,
// is3D, facing: "front"|"side"} or null after a message.
RK.ik.analyse = function (comp, layers) {
    var n = layers.length;
    if (n < 2 || n > 5) {
        RK.msg("Select the bones (2 to 4 layers), then the controller, before creating an IK.");
        return null;
    }
    var ctrl, chain;
    if (n === 2) {
        chain = [layers[0]];
        ctrl = layers[1];
    } else {
        var r = RK.ik.byParenting(layers);
        if (r) {
            chain = r.chain;
            ctrl = r.ctrl;
        } else {
            // selection order: bones from the tip to the root, then the controller
            ctrl = layers[n - 1];
            chain = [];
            for (var i = n - 2; i >= 0; i--) chain.push(layers[i]);
        }
    }
    var a = { chain: chain, ctrl: ctrl, clockwise: false, is3D: false, facing: "front" };
    var cw = RK.world(ctrl).pos;
    var near = function (L) { return Math.round(RK.dist(RK.world(L).pos, cw)) < 10; };
    if (n === 2) a.kind = "1";
    else if (n === 3) a.kind = near(chain[1]) ? "1+1" : "2";
    else if (n === 4) a.kind = near(chain[2]) ? "2+1" : "3";
    else a.kind = "3+1";
    // the side the chain bends to, from the pose
    if (chain.length >= 2) {
        var A = RK.world(chain[0]).pos, M = RK.world(chain[1]).pos;
        var E = [cw[0] - A[0], cw[1] - A[1]], V = [M[0] - A[0], M[1] - A[1]];
        // (15.52 also calls it not clockwise when E.x is 0, which flips a chain whose controller
        // is straight below or above its root: the cross product alone decides here)
        a.clockwise = E[0] * V[1] - E[1] * V[0] < 0;
        a.mirror = RK.ik.mirrored(chain[0]);
    }
    // 3D: the side it bends to in the plane of the root's frame (where the IK works)
    if (chain.length >= 2 && RK.is3D(chain[0])) {
        var A3 = RK.ik.toLayer(chain[0], RK.world(chain[0]).pos), M3 = RK.ik.toLayer(chain[0], RK.world(chain[1]).pos), C3 = RK.ik.toLayer(chain[0], cw);
        var E3 = [C3[0] - A3[0], C3[1] - A3[1]], V3 = [M3[0] - A3[0], M3[1] - A3[1]], s3 = chain[0].transform.scale.value;
        a.clockwise = (E3[0] * V3[1] - E3[1] * V3[0]) * (s3[0] * s3[1] < 0 ? -1 : 1) < 0;
        a.mirror = false;
    }
    // 3D: every selected layer 3D, the root facing the front or a side
    var all3D = true;
    for (var k = 0; k < layers.length; k++) if (!RK.is3D(layers[k])) all3D = false;
    if (all3D) {
        var o = RK.probe(chain[0], "var x = L.toWorldVec([1, 0, 0]), z = L.toWorldVec([0, 0, 1]);\n" +
            "[Math.atan2(-z[1], Math.sqrt(z[0] * z[0] + z[2] * z[2])) * 180 / Math.PI, Math.atan2(z[0], z[2]) * 180 / Math.PI, Math.atan2(x[1], x[0]) * 180 / Math.PI, 0]");
        var ax = Math.abs(Math.round(o[0])) % 180, ay = Math.abs(Math.round(o[1]));
        if (ax <= 10 || ax >= 170) {
            a.is3D = (ay % 90 <= 10 || ay % 90 >= 80);
            a.facing = (ay % 180 <= 10 || ay % 180 >= 170) ? "front" : "side";
        }
    }
    return a;
};

// A 3D layer parented in a chain keeps its place with an Orientation that turns it about Z only
// (EffectCraft puts the parent's turn there): that turn goes into its Z Rotation (the same
// transform), so the IK, which sets Z Rotation in the chain's plane, starts from the pose.
RK.ik.fold3D = function (L) {
    try {
        if (!RK.is3D(L)) return;
        var o = L.transform.property("ADBE Orientation"), r = L.transform.property("ADBE Rotate Z");
        if (!o || !r || o.numKeys || o.expression || r.numKeys || r.expression) return;
        var v = o.value;
        if (Math.abs(v[0]) > 1e-6 || Math.abs(v[1]) > 1e-6 || Math.abs(v[2]) < 1e-9) return;
        RK.withUnlocked(L, function () {
            r.setValue(r.value + v[2]);
            o.setValue([0, 0, 0]);
        });
    } catch (e) {}
};

// Is the root's parent chain mirrored (a negative scale somewhere)? Then the bend side seen in
// the comp is the other way round from the "Clockwise" parameter.
RK.ik.mirrored = function (root) {
    var p = root.parent;
    return p ? RK.mirrorSign(p) < 0 : false;
};

// Root, chain and controller from the parenting of 3 to 5 layers, or null when it doesn't
// make one simple chain.
RK.ik.byParenting = function (layers) {
    var ids = {}, i, j;
    for (i = 0; i < layers.length; i++) ids[layers[i].id] = layers[i];
    var parentOf = function (L) { var p = L.parent; return p && ids[p.id] ? p : null; };
    var ctrl = null, nCtrl = 0;
    for (i = 0; i < layers.length; i++) {
        var L = layers[i], linked = !!parentOf(L);
        for (j = 0; j < layers.length && !linked; j++) {
            var pj = layers[j].parent;
            if (j !== i && pj && pj.id === L.id) linked = true;
        }
        if (!linked) {
            ctrl = L;
            nCtrl++;
        }
    }
    if (nCtrl !== 1) return null;
    var rest = [];
    for (i = 0; i < layers.length; i++) if (layers[i] !== ctrl) rest.push(layers[i]);
    var root = null, nRoot = 0;
    for (i = 0; i < rest.length; i++) if (!parentOf(rest[i])) { root = rest[i]; nRoot++; }
    if (nRoot !== 1) return null;
    var chain = [root];
    while (chain.length < rest.length) {
        var last = chain[chain.length - 1], kids = [];
        for (i = 0; i < rest.length; i++) {
            var p = rest[i].parent;
            if (p && p.id === last.id) kids.push(rest[i]);
        }
        if (kids.length !== 1) return null;   // a gap or a branch
        chain.push(kids[0]);
    }
    return { chain: chain, ctrl: ctrl };
};

// ---------------------------------------------------------------------------- expression helpers
//
// Small reads for expressions that don't go through RK.rkx (RK.xFast must be in the text):
// `var v = rk(function () { return ...; });`, null while the value is on its way.

RK.ik.read = function (v, dep, code) {
    return "var " + v + " = " + (dep ? dep + " == null ? null : " : "") + "rk(function () { return " + code + "; });\n";
};
RK.ik.layer = function (v, layer) {
    return RK.ik.read(v, null, "rkLayer(" + RK.q(layer.name) + ")");
};
// A transform property's value (match id: position, scale, anchor, rotation).
RK.ik.tprop = function (v, L, id, api) {
    return RK.ik.read(v, L, "rkVal(" + L + ", \"transform/" + id + "\", function (L) { return L.transform." + api + ".value; })");
};
// An effect's first parameter, by the effect's name.
RK.ik.fx = function (v, L, effectName) {
    return RK.ik.read(v, L, "rkVal(" + L + ", " + RK.q(RK.fxPath(effectName)) + ", function (L) { return L.effect(" + RK.q(effectName) + ")(1).value; })");
};
RK.ik.param = function (v, owner, group, param) {
    return RK.ik.fx(v, owner, RK.fxg(group, param));
};

// ---------------------------------------------------------------------------- building
//
// The IKs register their formulas with RK.rkx (46_rkx.js), which writes the expressions.

// The IK effect group on the controller (spec 00 §6.9: one-, two- and three-layer IK effects).
RK.ik.addEffect = function (ctrl, name, kind, clockwise) {
    if (kind === 1) {
        return RK.addGroup(ctrl, name, [
            { name: "Weight", type: "slider", value: 100 },
            { name: "Reverse", type: "checkbox", value: clockwise },
            { name: "FK", type: "angle", value: 0 }
        ]);
    }
    var p = [{ name: "Clockwise", type: "checkbox", value: clockwise }, { name: "Disable IK", type: "checkbox", value: false },
        { name: "Upper FK", type: "angle", value: 0 }];
    if (kind === 3) p.push({ name: "Middle FK", type: "angle", value: 0 });
    p.push({ name: "Lower FK", type: "angle", value: 0 }, { name: "Auto-Stretch", type: "checkbox", value: true },
        { name: "Stretch", type: "slider", value: 0 }, { name: "Auto-Shrink", type: "slider", value: 0 });
    return RK.addGroup(ctrl, name, p);
};

RK.ik.PARAMS2 = ["Clockwise", "Disable IK", "Upper FK", "Lower FK", "Auto-Stretch", "Stretch", "Auto-Shrink"];
RK.ik.PARAMS3 = ["Clockwise", "Disable IK", "Upper FK", "Middle FK", "Lower FK", "Auto-Stretch", "Stretch", "Auto-Shrink"];

RK.ik.tag = function (ctrl, text) {
    try {
        var c = ctrl.comment || "";
        if (c.indexOf(text) < 0) ctrl.comment = c + text;
    } catch (e) {}
};

// One-bone IK: bone looks at the controller. Returns the effect group.
// opts: {name, weight (%), extraFK: [RK.rkx.fx refs added to FK]}
RK.ik.one = function (comp, bone, ctrl, opts) {
    opts = opts || {};
    var bw = RK.world(bone).pos, cw = RK.world(ctrl).pos;
    var reverse = cw[0] < bw[0];
    var name = RK.uniqueEffectName(ctrl, opts.name || "IK " + bone.name);
    var grp = RK.ik.addEffect(ctrl, name, 1, reverse);
    if (opts.weight !== undefined) grp.fx.Weight.property(1).setValue(opts.weight);
    RK.rkx.with(comp, function (b) {
        b.set(bone, "r", { k: "look", c: ctrl, w: RK.rkx.fx(ctrl, RK.fxg(name, "Weight")), v: RK.rkx.fx(ctrl, RK.fxg(name, "Reverse")),
            f: RK.rkx.fx(ctrl, RK.fxg(name, "FK")), x: opts.extraFK || undefined }, "IK " + ctrl.name + " look-at");
    });
    RK.ik.tag(ctrl, "[IK " + bone.name + "] ");
    return grp;
};

// IK Goal: the goal keeps its orientation (plus the controller's rotation). ctrl may be null.
// Returns the switch's effect name.
RK.ik.goal = function (comp, goal, ctrl) {
    var owner = ctrl || goal;
    var sw = RK.uniqueEffectName(owner, ctrl ? "Goal " + goal.name : "Goal");
    RK.fxOff(owner, RK.addCheckbox(owner, sw, true));
    RK.rkx.with(comp, function (b) {
        b.set(goal, "r", { k: "goal", c: ctrl || null, s: RK.rkx.fx(owner, sw) }, "IK Goal " + goal.name);
    });
    if (ctrl) RK.ik.tag(ctrl, "[Goal " + goal.name + "] ");
    return sw;
};

// Two- or three-bone IK, optionally with a goal. a: the analysis (chain, ctrl, clockwise...).
// opts: {fxLayer: the layer holding the IK parameters when it isn't the controller}
RK.ik.multi = function (comp, bones, ctrl, a, goal, opts) {
    opts = opts || {};
    var F = opts.fxLayer || ctrl;
    var n = bones.length, i;
    // parenting: each bone the child of the previous one
    for (i = n - 1; i >= 1; i--) {
        var pi = bones[i - 1].parent;
        if (pi && pi.id === bones[i].id) RK.withUnlocked(bones[i - 1], function () { bones[i - 1].parent = null; });
    }
    for (i = 1; i < n; i++) {
        var b = bones[i], pb = bones[i - 1];
        if (!b.parent || b.parent.id !== pb.id) RK.withUnlocked(b, function () { b.parent = pb; });
    }
    for (i = 0; i < n; i++) RK.ik.fold3D(bones[i]);
    var last = bones[n - 1];
    // rest geometry: anchors, and where each bone ends in its own layer space
    var anchors = [], ends = [];
    for (i = 0; i < n; i++) {
        var ap = bones[i].transform.anchorPoint.value;
        anchors.push([ap[0], ap[1]]);
        if (i < n - 1) {
            var np = bones[i + 1].transform.position;
            var nv = np.numKeys ? np.keyValue(1) : np.value;
            ends.push([nv[0], nv[1]]);
        }
    }
    // the tip: the controller's world position, in the last bone's layer space
    var tip = RK.ik.toLayer(last, RK.world(ctrl).pos);
    ends.push(tip);
    var clockwise = a.mirror ? !a.clockwise : a.clockwise;
    var group = RK.uniqueEffectName(F, "IK " + last.name);
    RK.ik.addEffect(F, group, n, clockwise);
    var limitName = RK.uniqueEffectName(bones[0], "IK Limit");
    RK.fxOff(bones[0], RK.addAngle(bones[0], limitName, 0));
    // the tip as a point on the last bone. Without a goal it follows the stretched tip (the
    // only place it shows); with one it stays at rest, since the goal shows the tip and
    // EffectCraft evaluates every expression of a layer, effects switched off included, each
    // time it renders it.
    var endName = RK.uniqueEffectName(last, "IK End");
    var off = RK.effectOffset(last);
    var endFx = RK.addPoint(last, endName, [tip[0] + off[0], tip[1] + off[1]]);
    RK.fxOff(last, endFx);
    var names = n === 3 ? RK.ik.PARAMS3 : RK.ik.PARAMS2, g = [];
    for (i = 0; i < names.length; i++) g.push(RK.rkx.fx(F, RK.fxg(group, names[i])));
    var q = { b: bones.slice(), c: ctrl, g: g, l: RK.rkx.fx(bones[0], limitName), A: anchors, E: ends };
    var label = "IK " + ctrl.name, sw = null;
    if (goal && !(goal.parent && goal.parent.id === last.id)) RK.withUnlocked(goal, function () { goal.parent = last; });
    if (goal) RK.ik.fold3D(goal);
    if (goal) {
        sw = RK.uniqueEffectName(ctrl, "Goal " + goal.name);
        RK.fxOff(ctrl, RK.addCheckbox(ctrl, sw, true));
    }
    RK.rkx.with(comp, function (bd) {
        for (var j = 0; j < n; j++) bd.set(bones[j], "r", { k: "ik", q: q, o: "r" + j }, label + " rotation " + (j + 1));
        for (j = 1; j < n; j++) bd.set(bones[j], "p", { k: "ik", q: q, o: "p" + j }, label + " stretch " + (j + 1));
        if (!goal) bd.set(last, "x", { k: "ik", q: q, o: "ep", e: off }, label + " end", endFx.property(1));
        if (goal) {
            bd.set(goal, "p", { k: "ik", q: q, o: "gp" }, label + " goal position");
            bd.set(goal, "r", { k: "goal", c: ctrl, s: RK.rkx.fx(ctrl, sw) }, label + " goal");
        }
    });
    RK.ik.tag(ctrl, "[IK " + bones[0].name + " > " + last.name + "] ");
    if (F !== ctrl) RK.ik.tag(F, "[IK " + bones[0].name + " > " + last.name + "] ");
    return { bones: bones, ctrl: ctrl, group: group, goal: goal, goalSwitch: sw, solver: q };
};

// A world point in a layer's own space (2D), now.
RK.ik.toLayer = function (layer, w) {
    var r = RK.probe(layer, "var p = L.fromWorld([" + w[0] + ", " + w[1] + ", " + (w[2] || 0) + "]);\n[p[0], p[1], 0, 0]");
    return [r[0], r[1]];
};

// Build the IK the analysis describes (kind may have been changed on the page).
// opts: {noLockShy (the Auto-Rig never locks), fxLayer (see RK.ik.multi)}
RK.ik.build = function (comp, a, kind, opts) {
    opts = opts || {};
    RK.nameCheck(comp);
    var chain = a.chain, ctrl = a.ctrl, goal = null, bones;
    if (kind === "1") bones = [chain[0]];
    else if (kind === "1+1") { bones = [chain[0]]; goal = chain[1]; }
    else if (kind === "2") bones = [chain[0], chain[1]];
    else if (kind === "2+1") { bones = [chain[0], chain[1]]; goal = chain[2]; }
    else if (kind === "3") bones = [chain[0], chain[1], chain[2]];
    else { bones = [chain[0], chain[1], chain[2]]; goal = chain[3]; }
    RK.rkx.with(comp, function () {
        if (bones.length === 1) {
            RK.ik.one(comp, bones[0], ctrl);
            if (goal) {
                RK.withUnlocked(bones[0], function () {
                    if (bones[0].parent && bones[0].parent.id === goal.id) bones[0].parent = null;
                });
                RK.withUnlocked(goal, function () { goal.parent = bones[0]; });
                RK.ik.fold3D(bones[0]);
                RK.ik.fold3D(goal);
                RK.ik.goal(comp, goal, ctrl);
            }
        } else {
            RK.ik.multi(comp, bones, ctrl, a, goal, opts);
        }
    });
    if (!opts.noLockShy && RK.pref("ikLockShy", false)) {
        var all = bones.slice();
        if (goal) all.push(goal);
        for (var i = 0; i < all.length; i++) {
            all[i].shy = true;
            all[i].locked = true;
        }
    }
    RK.selectOnly(comp, [ctrl]);
    return { bones: bones, goal: goal, ctrl: ctrl };
};

// ---------------------------------------------------------------------------- the IK tool

RK.ik.KINDS = {
    "1": { label: "1-Layer IK (LookAt)", short: "1" },
    "1+1": { label: "1-Layer IK & Goal", short: "1+1" },
    "2": { label: "2-Layer IK", short: "2" },
    "2+1": { label: "2-Layer IK & Goal", short: "2+1" },
    "3": { label: "3-Layer IK", short: "3" },
    "3+1": { label: "3-Layer IK & Goal", short: "3+1" }
};

// A drawing of a chain for the IK page (bones, joints, the controller, a goal).
RK.ik.picture = function (kind) {
    var ops = [];
    var pts = kind === "1" || kind === "1+1" ? [[14, 74], [62, 38]] :
        kind === "2" || kind === "2+1" ? [[12, 80], [42, 30], [76, 62]] : [[10, 82], [24, 38], [64, 30], [80, 70]];
    var flat = [];
    for (var i = 0; i < pts.length; i++) flat.push(pts[i][0], pts[i][1]);
    ops.push(["L"].concat(flat));
    for (i = 0; i < pts.length - 1; i++) ops.push(["D", pts[i][0], pts[i][1], 3]);
    var e = pts[pts.length - 1];
    if (kind.indexOf("+1") > 0) ops.push(["F", e[0] - 4, e[1] - 4, 14, 8]);
    var cx = kind === "1" || kind === "1+1" ? e[0] + 22 : e[0] + (kind.indexOf("+1") > 0 ? 6 : 0);
    var cy = kind === "1" || kind === "1+1" ? e[1] - 18 : e[1];
    ops.push(["C", cx, cy, 9], ["L", cx - 13, cy, cx + 13, cy], ["L", cx, cy - 13, cx, cy + 13]);
    return ops;
};

RK.ik.pictureBox = function (parent, kind, radio) {
    var col = parent.add("group");
    col.orientation = "column";
    col.alignChildren = ["center", "top"];
    col.spacing = 2;
    if (!RK.ui.expert) {
        var pic = col.add("statictext", undefined, "");
        pic.preferredSize = [100, 100];
        pic.onDraw = function () {
            var g = this.graphics;
            var on = !radio || radio.value;
            g.newPath();
            g.rectPath(0.5, 0.5, 99, 99);
            g.strokePath(g.newPen(g.PenType.SOLID_COLOR, RK.C.rule, 1));
            RK.drawIcon(g, RK.ik.picture(kind), 0, 0, on ? RK.C.icon : RK.C.dim);
        };
    }
    return col;
};

RK.tool("ik", {
    label: "IK", icon: RK.I.ik,
    tip: "IK: select the bones (from the tip to the root, or a parented chain) and the controller last, then click.",
    page: function (body) {
        var comp = RK.comp();
        if (!comp) { RK.ui.back(); return; }
        var a = RK.ik.analyse(comp, comp.selectedLayers);
        if (!a) { RK.ui.back(); return; }
        var kinds = a.kind === "1" ? ["1"] : a.kind === "3+1" ? ["3+1"] :
            (a.kind === "1+1" || a.kind === "2") ? ["1+1", "2"] : ["3", "2+1"];
        var state = { kind: a.kind };
        var row = body.add("group");
        row.alignment = ["center", "top"];
        row.spacing = 16;
        var radios = [];
        for (var i = 0; i < kinds.length; i++) {
            (function (k) {
                var label = RK.ui.expert ? RK.ik.KINDS[k].short : RK.ik.KINDS[k].label;
                var holder = { value: k === state.kind };
                var col = RK.ik.pictureBox(row, k, kinds.length > 1 ? holder : null);
                if (kinds.length > 1) {
                    var rb = RK.ui.radio(col, label, k === state.kind);
                    rb.onClick = function () {
                        state.kind = k;
                        for (var j = 0; j < radios.length; j++) {
                            radios[j].rb.value = radios[j].k === k;
                            radios[j].holder.value = radios[j].k === k;
                        }
                    };
                    radios.push({ rb: rb, k: k, holder: holder });
                } else {
                    RK.ui.label(col, label);
                }
            })(kinds[i]);
        }
        RK.ui.separator(body);
        var ls = RK.ui.check(body, "Lock and shy layers after creation", RK.pref("ikLockShy", false));
        ls.onClick = function () { RK.setPref("ikLockShy", this.value); };
        if (a.is3D) {
            // (the IK works in the plane the chain lies in, whatever the view: nothing to choose)
            RK.ui.separator(body, "3D");
            RK.ui.label(body, (a.facing === "side" ? "Left/Right view" : "Front/Back view") + ": the chain turns in its own plane.");
        }
        RK.ui.bottomRow(body, [
            RK.ui.backItem("Cancel"),
            {
                label: "Create", icon: RK.I.ok, name: "create",
                onClick: RK.ui.action("IK", function () {
                    var c = RK.comp();
                    if (!c) return false;
                    RK.undo(RK.undoName("IK"), function () { RK.ik.build(c, a, state.kind); });
                    return true;
                }, true)
            }
        ]);
    }
});

// ============================================================================ Rigging tab: IK Goal, Bezier IK, Controllers, Zero

// ---------------------------------------------------------------------------- IK Goal

RK.tool("ikgoal", {
    label: "IK Goal", icon: RK.I.ikgoal,
    tip: "IK Goal: the selected layer keeps its orientation whatever its parents do. Select the layer, and optionally its controller second.",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var sel = comp.selectedLayers;
        if (sel.length < 1 || sel.length > 2) {
            RK.msg("Select the layer (and optionally its controller, second) before applying IK Goal.");
            return;
        }
        var goal = sel[0], ctrl = sel.length === 2 ? sel[1] : null;
        RK.undo(RK.undoName("IK Goal"), function () {
            RK.nameCheck(comp);
            RK.ik.goal(comp, goal, ctrl);
            if (ctrl) RK.selectOnly(comp, [ctrl]);
        });
    }
});

// ---------------------------------------------------------------------------- Bezier IK

RK.bz = {};

// Build a Bezier IK. bones: from the tip to the root; E: end controller; R: root controller.
// The bones are un-parented; each one's position follows the curve and its rotation the chain
// (RK.rkx formulas "bz"). Returns {K1, K2, group}.
RK.bz.build = function (comp, bones, E, R, cubic) {
    var pE = RK.world(E).pos, pR = RK.world(R).pos;
    RK.nameCheck(comp);
    var flags = { rotation: false, x: true, y: true, scale: false };
    var K1, K2 = null;
    if (!cubic) {
        K1 = RK.ctrl.create(comp, E, { flags: flags, position: [(pE[0] + pR[0]) / 2, (pE[1] + pR[1]) / 2], name: RK.uniqueName(comp, E.name + "_curve"), select: false });
    } else {
        K1 = RK.ctrl.create(comp, E, { flags: flags, position: [(2 * pE[0] + pR[0]) / 3, (2 * pE[1] + pR[1]) / 3], name: RK.uniqueName(comp, E.name + "_curve"), select: false });
        K2 = RK.ctrl.create(comp, R, { flags: flags, position: [(pE[0] + 2 * pR[0]) / 3, (pE[1] + 2 * pR[1]) / 3], name: RK.uniqueName(comp, R.name + "_curve"), select: false });
    }
    var group = RK.uniqueEffectName(E, "Bezier IK " + E.name);
    RK.addGroup(E, group, [{ name: "Offset", type: "slider", value: 0 }, { name: "Auto Orientation", type: "checkbox", value: true }]);
    var n = bones.length + 2, i;
    var z = { e: E, r: R, k1: K1, k2: K2, o: RK.rkx.fx(E, RK.fxg(group, "Offset")), a: RK.rkx.fx(E, RK.fxg(group, "Auto Orientation")),
        b: bones.slice(), w: [], t: [], p: [] };
    for (i = 0; i < bones.length; i++) {
        (function (bone, i) {
            RK.withUnlocked(bone, function () {
                if (bone.parent) bone.parent = null;
                var ownName = RK.uniqueEffectName(bone, "Bezier IK Offset");
                RK.fxOff(bone, RK.addSlider(bone, ownName, 0));
                var pos = bone.transform.position;
                var p0 = pos.numKeys ? pos.keyValue(1) : pos.value;
                z.w.push(RK.rkx.fx(bone, ownName));
                z.t.push(i / (n - 2) + 1.5 / n);
                z.p.push([p0[0], p0[1]]);
            });
        })(bones[i], i);
    }
    RK.rkx.with(comp, function (bd) {
        for (var j = 0; j < bones.length; j++) {
            bd.set(bones[j], "p", { k: "bz", b: z, i: j, o: "p" }, "Bezier IK " + E.name + " position");
            bd.set(bones[j], "r", { k: "bz", b: z, i: j, o: "r" }, "Bezier IK " + E.name + " rotation");
        }
    });
    var tag = "[Bezier IK " + bones[bones.length - 1].name + " > " + bones[0].name + "] ";
    var ks = [R, E, K1];
    if (K2) ks.push(K2);
    for (i = 0; i < ks.length; i++) RK.ik.tag(ks[i], tag);
    if (RK.pref("ikLockShy", false)) {
        for (i = 0; i < bones.length; i++) {
            bones[i].shy = true;
            bones[i].locked = true;
        }
    }
    RK.selectOnly(comp, [K2 || K1]);
    return { K1: K1, K2: K2, group: group };
};

RK.bz.picture = function (cubic) {
    var ops = [];
    var pts = cubic ? RK.bez(10, 80, 30, 10, 70, 90, 90, 20, 16) : RK.bez(10, 80, 40, 0, 60, 0, 90, 80, 16);
    ops.push(["L"].concat(pts));
    for (var i = 2; i < pts.length - 2; i += 6) ops.push(["D", pts[i], pts[i + 1], 3]);
    ops.push(["C", 10, 80, 6], ["C", 90, cubic ? 20 : 80, 6]);
    if (cubic) ops.push(["R", 26, 6, 8, 8], ["R", 66, 86, 8, 8]);
    else ops.push(["R", 46, 0, 8, 8]);
    return ops;
};

RK.tool("bezierik", {
    label: "Bezier IK", icon: RK.I.bezierik,
    tip: "Bezier IK: moves layers along a curve. Select the layers from the tip to the root, then the end controller, then the root controller.",
    page: function (body) {
        var state = { cubic: false };
        var row = body.add("group");
        row.alignment = ["center", "top"];
        row.spacing = 16;
        var radios = [];
        var mk = function (cubic) {
            var col = row.add("group");
            col.orientation = "column";
            col.alignChildren = ["center", "top"];
            col.spacing = 2;
            if (!RK.ui.expert) {
                var pic = col.add("statictext", undefined, "");
                pic.preferredSize = [100, 100];
                pic.onDraw = function () {
                    var g = this.graphics;
                    g.newPath();
                    g.rectPath(0.5, 0.5, 99, 99);
                    g.strokePath(g.newPen(g.PenType.SOLID_COLOR, RK.C.rule, 1));
                    RK.drawIcon(g, RK.bz.picture(cubic), 0, 0, state.cubic === cubic ? RK.C.icon : RK.C.dim);
                };
            }
            var rb = RK.ui.radio(col, RK.ui.expert ? (cubic ? "Cub." : "Sim.") : (cubic ? "Cubic" : "Simple"), state.cubic === cubic,
                cubic ? "Two curve controllers: S shapes" : "One curve controller: C shapes");
            rb.onClick = function () {
                state.cubic = cubic;
                for (var i = 0; i < radios.length; i++) radios[i].value = (i === 1) === cubic;
            };
            radios.push(rb);
        };
        mk(false);
        mk(true);
        RK.ui.bottomRow(body, [
            RK.ui.backItem("Cancel"),
            {
                label: "Create", icon: RK.I.ok, name: "create",
                onClick: RK.ui.action("Bezier IK", function () {
                    var comp = RK.comp();
                    if (!comp) return false;
                    var sel = comp.selectedLayers;
                    if (sel.length < 3) {
                        RK.msg("Select the layers (from the tip to the root), then the end controller, then the root controller.");
                        return false;
                    }
                    var bones = sel.slice(0, sel.length - 2), E = sel[sel.length - 2], R = sel[sel.length - 1];
                    RK.undo(RK.undoName("Bezier IK"), function () { RK.bz.build(comp, bones, E, R, state.cubic); });
                    return true;
                }, true)
            }
        ]);
    }
});

// ---------------------------------------------------------------------------- Zero

RK.tool("zero", {
    label: "Zero", icon: RK.I.zero,
    tip: "Zero: adds a parent null so the layer's transform reads zero (position 0, rotation 0, scale 100%).",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var sel = comp.selectedLayers;
        if (!sel.length) {
            RK.msg("No layer is selected. Select a layer.");
            return;
        }
        RK.undo(RK.undoName("Zero"), function () {
            for (var i = 0; i < sel.length; i++) RK.zero(sel[i]);
        });
    }
});

// ---------------------------------------------------------------------------- Controllers

RK.cp = {};
RK.cp.COLORS = [["Red", [1, 0, 0]], ["Green", [0, 1, 0]], ["Blue", [0, 0, 1]], ["Cyan", [0, 1, 1]], ["Magenta", [1, 0, 1]],
    ["Yellow", [1, 1, 0]], ["White", [1, 1, 1]], ["Light Gray", [0.7, 0.7, 0.7]], ["Dark Gray", [0.25, 0.25, 0.25]], ["Black", [0, 0, 0]]];
RK.cp.state = { shape: 0, rotation: true, x: true, y: true, scale: false, autoLock: false };

RK.cp.hex = function (c) {
    var h = "";
    for (var i = 0; i < 3; i++) {
        var v = Math.max(0, Math.min(255, Math.round(c[i] * 255))).toString(16).toUpperCase();
        h += v.length < 2 ? "0" + v : v;
    }
    return h;
};
RK.cp.fromHex = function (s) {
    s = String(s).replace(/[^0-9a-fA-F]/g, "");
    if (s.length === 3) s = s.charAt(0) + s.charAt(0) + s.charAt(1) + s.charAt(1) + s.charAt(2) + s.charAt(2);
    if (s.length !== 6) return null;
    return [parseInt(s.substr(0, 2), 16) / 255, parseInt(s.substr(2, 2), 16) / 255, parseInt(s.substr(4, 2), 16) / 255];
};
RK.cp.flags = function () {
    var st = RK.cp.state;
    var f = { rotation: st.rotation, x: st.x, y: st.y, scale: st.scale, arc: st.shape === 1, eye: st.shape === 2, camera: st.shape === 3 };
    return f;
};
RK.cp.lockFlags = function () {
    var st = RK.cp.state;
    return { rotation: st.rotation, x: st.x, y: st.y, scale: st.scale, arc: false, eye: false, camera: false };
};

// The controllers to act on: the selected ones, or every controller of the comp.
RK.cp.targets = function (comp) {
    var sel = comp.selectedLayers, out = [], i;
    if (sel.length) {
        for (i = 0; i < sel.length; i++) if (RK.ctrl.isController(sel[i])) out.push(sel[i]);
        return out;
    }
    for (i = 1; i <= comp.numLayers; i++) if (RK.ctrl.isController(comp.layer(i))) out.push(comp.layer(i));
    return out;
};

RK.cp.create = function (comp) {
    var sel = comp.selectedLayers, made = [];
    var f = RK.cp.flags(), lf = RK.cp.lockFlags();
    var layers = sel.length ? sel : [null];
    for (var i = 0; i < layers.length; i++) {
        var c = RK.ctrl.create(comp, layers[i], { flags: f, select: false });
        if (RK.cp.state.autoLock) RK.ctrl.lock(c, lf);
        made.push(c);
    }
    RK.selectOnly(comp, made);
    return made;
};

RK.cp.update = function (comp) {
    var list = RK.cp.targets(comp), st = RK.ctrl.settings(), n = 0, skipped = 0;
    var f = RK.cp.flags(), lf = RK.cp.lockFlags();
    var size = RK.ctrl.sizePx(comp, st);
    for (var i = 0; i < list.length; i++) {
        var c = list[i];
        if (!(c instanceof ShapeLayer)) {
            skipped++;
            continue;
        }
        RK.withUnlocked(c, function () {
            RK.ctrl.unlock(c);
            RK.ctrl.draw(c, f, size, st.color);
            c.guideLayer = true;
            if (RK.cp.state.autoLock) RK.ctrl.lock(c, lf);
        });
        n++;
    }
    if (skipped) RK.msg(n + " controller(s) updated; " + skipped + " null controller(s) can't hold a drawing and were left as they are.");
    return n;
};

RK.cp.each = function (name, fn) {
    var comp = RK.comp();
    if (!comp) return;
    var list = RK.cp.targets(comp);
    if (!list.length) {
        RK.msg("No controller found: select controllers, or none to act on every controller of the comp.");
        return;
    }
    RK.undo(RK.undoName(name), function () {
        for (var i = 0; i < list.length; i++) (function (c) { RK.withUnlocked(c, function () { fn(c, comp); }); })(list[i]);
    });
};

RK.cp.reset = function (c, comp) {
    var tr = c.transform;
    var set = function (p, v) {
        if (p && p.numKeys === 0) {
            try {
                p.setValue(v);
            } catch (e) {}
        }
    };
    if (tr.position.dimensionsSeparated) {
        set(tr.property("ADBE Position_0"), c.parent ? 0 : comp.width / 2);
        set(tr.property("ADBE Position_1"), c.parent ? 0 : comp.height / 2);
    } else set(tr.position, c.parent ? [0, 0, 0] : [comp.width / 2, comp.height / 2, 0]);
    set(tr.scale, [100, 100, 100]);
    set(RK.rotProp(c), 0);
    set(tr.opacity, 100);
};

// Small drawings for the flag rows of the Controllers page (20 x 16).
RK.cp.I = {
    rotation: [RK.op("L", RK.arc(10, 8, 6, 140, 400, 12)), ["L", 13, 2, 16.5, 4.5, 13.5, 7.5]],
    x: [["L", 2, 8, 18, 8], ["L", 5, 5, 2, 8, 5, 11], ["L", 15, 5, 18, 8, 15, 11]],
    y: [["L", 10, 1, 10, 15], ["L", 7, 4, 10, 1, 13, 4], ["L", 7, 12, 10, 15, 13, 12]],
    scale: [["R", 3, 2, 12, 12], ["L", 11, 10, 17, 15], ["L", 13, 15, 17, 15, 17, 11]],
    unlock: [["F", 4, 8, 12, 7], RK.op("L", RK.arc(15, 6, 3.4, 180, 360, 8)), ["L", 11.6, 6, 11.6, 8]],
    lock: [["F", 4, 8, 12, 7], RK.op("L", RK.arc(10, 6, 3.4, 180, 360, 8)), ["L", 6.6, 6, 6.6, 8], ["L", 13.4, 6, 13.4, 8]],
    show: [["E", 10, 8, 8, 4.5], ["D", 10, 8, 2.2]],
    hide: [["E", 10, 8, 8, 4.5], ["L", 3, 14, 17, 2]],
    reset: [["T", "0", 6, 0, 13], ["L", 2, 14, 18, 14]]
};

RK.cp.flagRow = function (parent, key, label, tip) {
    var r = parent.add("group");
    r.orientation = "row";
    r.alignChildren = ["left", "center"];
    r.spacing = 4;
    var cb = r.add("checkbox", undefined, "");
    cb.preferredSize = [20, 18];
    cb.value = !!RK.cp.state[key];
    cb.helpTip = tip;
    var ic = r.add("statictext", undefined, "");
    ic.preferredSize = [RK.ui.expert ? 20 : 20 + 6 + Math.ceil(RK.textWidth(label, RK.FONT)), 18];
    ic.helpTip = tip;
    ic.onDraw = function () {
        var on = cb.enabled !== false;
        RK.drawIcon(this.graphics, RK.cp.I[key], 0, 1, on ? RK.C.icon : RK.C.dim);
        if (!RK.ui.expert) this.graphics.drawString(label, on ? null : this.graphics.newPen(this.graphics.PenType.SOLID_COLOR, RK.C.dim, 1), 26, 1);
    };
    cb.onClick = function () { RK.cp.state[key] = this.value; };
    return cb;
};

RK.cp.open = function () {
    if (RK.ctrl.settings().type === "null") {
        // Null controllers: no options to choose, create them at once.
        var comp = RK.comp();
        if (!comp) return;
        RK.undo(RK.undoName("Controllers"), function () {
            var sel = comp.selectedLayers, made = [];
            var layers = sel.length ? sel : [null];
            for (var i = 0; i < layers.length; i++) made.push(RK.ctrl.create(comp, layers[i], { select: false }));
            RK.selectOnly(comp, made);
        });
        return;
    }
    RK.ui.openPage(RK.tools.controllers);
};

RK.tool("controllers", {
    label: "Controllers", icon: RK.I.controllers,
    tip: "Controllers: creates controllers for the selected layers (or one at the comp centre), and locks, hides or resets existing ones.",
    run: function () { RK.cp.open(); },
    page: null
});
RK.tools.controllers.pageFn = function (body) {
    var st = RK.cp.state, cs = RK.ctrl.settings();
    var top = body.add("group");
    top.orientation = "row";
    top.alignChildren = ["left", "top"];
    top.spacing = 14;
    // shape and flags
    var left = top.add("group");
    left.orientation = "column";
    left.alignChildren = ["left", "top"];
    left.spacing = 3;
    var shape = RK.ui.drop(left, ["Transform", "Arc", "Eye", "Camera"], st.shape, 110, "The controller's drawing: Transform uses the four boxes below");
    var rows = [RK.cp.flagRow(left, "rotation", "Rotation", "Rotation"), RK.cp.flagRow(left, "x", "X position", "X position"),
        RK.cp.flagRow(left, "y", "Y position", "Y position"), RK.cp.flagRow(left, "scale", "Scale", "Scale")];
    var syncShape = function () {
        for (var i = 0; i < rows.length; i++) rows[i].enabled = st.shape === 0;
    };
    shape.onChange = function () {
        st.shape = this.selection ? this.selection.index : 0;
        syncShape();
    };
    syncShape();
    // size, colour, auto lock
    var right = top.add("group");
    right.orientation = "column";
    right.alignChildren = ["left", "top"];
    right.spacing = 3;
    var lw = RK.ui.expert ? 0 : 74;
    var r1 = RK.ui.row(right, RK.ui.expert ? "" : "Size", lw);
    var sizes = ["Small", "Medium", "Big", "Custom"];
    var sizeSel = !cs.autoSize ? 3 : cs.sizeHint === "small" ? 0 : cs.sizeHint === "big" ? 2 : 1;
    var size = RK.ui.drop(r1, sizes, sizeSel, 0, "Auto-size: from the comp size, or a custom size in pixels");
    var r2 = RK.ui.row(right, RK.ui.expert ? "" : "Custom size", lw);
    var csz = RK.ui.number(r2, cs.size, 50, "Custom size (pixels)");
    csz.enabled = sizeSel === 3;
    size.onChange = function () {
        var k = this.selection ? this.selection.index : 1;
        if (k === 3) RK.setPref("ctrlAutoSize", false);
        else {
            RK.setPref("ctrlAutoSize", true);
            RK.setPref("ctrlSizeHint", ["small", "medium", "big"][k]);
        }
        csz.enabled = k === 3;
    };
    csz.onChange = function () { RK.setPref("ctrlSize", Math.round(RK.ui.num(csz, 100, 1))); };
    var r3 = RK.ui.row(right, RK.ui.expert ? "" : "Color", lw);
    var names = [], ci = RK.cp.COLORS.length;
    for (var i = 0; i < RK.cp.COLORS.length; i++) {
        names.push(RK.cp.COLORS[i][0]);
        var c = RK.cp.COLORS[i][1];
        if (Math.abs(c[0] - cs.color[0]) < 0.01 && Math.abs(c[1] - cs.color[1]) < 0.01 && Math.abs(c[2] - cs.color[2]) < 0.01) ci = i;
    }
    names.push("Custom");
    var color = RK.ui.drop(r3, names, ci, 0, "Color");
    var r4 = RK.ui.row(right, RK.ui.expert ? "" : "Custom color", lw);
    RK.ui.label(r4, "#");
    var hex = RK.ui.text(r4, RK.cp.hex(cs.color), 64, "Custom color (hexadecimal RRGGBB)");
    hex.enabled = ci === RK.cp.COLORS.length;
    color.onChange = function () {
        var k = this.selection ? this.selection.index : 0;
        if (k < RK.cp.COLORS.length) {
            RK.setPref("ctrlColor", RK.cp.COLORS[k][1]);
            hex.text = RK.cp.hex(RK.cp.COLORS[k][1]);
        }
        hex.enabled = k === RK.cp.COLORS.length;
    };
    hex.onChange = function () {
        var c = RK.cp.fromHex(hex.text);
        if (c) RK.setPref("ctrlColor", c);
        else RK.msg("Type a colour as six hexadecimal digits, like FF8800.");
    };
    var al = RK.ui.check(right, "Auto Lock", st.autoLock, "Lock the transforms the controller isn't meant for. Unlock controllers before parenting them.");
    var warn = RK.ui.label(right, "Unlock locked controllers\nbefore parenting them.");
    warn.visible = st.autoLock && !RK.ui.expert;
    al.onClick = function () {
        st.autoLock = this.value;
        warn.visible = this.value && !RK.ui.expert;
        RK.ui.relayout();
    };
    // actions on existing controllers
    var acts = body.add("group");
    acts.alignment = ["center", "top"];
    acts.spacing = 6;
    var act = function (key, tip, name, fn) {
        RK.ui.flat(acts, { icon: RK.cp.I[key], iconW: 20, w: 28, h: 24, tip: tip, name: "ctrl." + key, onClick: function () {
            RK.clearMsg();
            try {
                RK.cp.each(name, fn);
            } catch (e) {
                RK.msg(name + ": " + RK.err(e));
            }
        } });
    };
    act("unlock", "Unlock the selected controllers (all if none is selected)", "Unlock Controllers", function (c) { RK.ctrl.unlock(c); });
    act("lock", "Lock the selected controllers (all if none is selected)", "Lock Controllers", function (c) { RK.ctrl.lock(c); });
    act("show", "Show the selected controllers (all if none is selected)", "Unhide Controllers", function (c) { c.enabled = true; });
    act("hide", "Hide the selected controllers (all if none is selected)", "Hide Controllers", function (c) { c.enabled = false; });
    act("reset", "Reset the selected controllers' transformations (all if none is selected)", "Reset Controllers", RK.cp.reset);
    RK.ui.bottomRow(body, [
        RK.ui.backItem("Back"),
        {
            label: "Update", icon: RK.I.update, name: "update", tip: "Redraw the selected controllers (all if none is selected) with these settings",
            onClick: RK.ui.action("Update", function () {
                var comp = RK.comp();
                if (!comp) return false;
                RK.undo(RK.undoName("Update Controllers"), function () { RK.cp.update(comp); });
                return true;
            }, true)
        },
        {
            label: "Create", icon: RK.I.ok, name: "create", tip: "Create a controller for each selected layer",
            onClick: RK.ui.action("Create", function () {
                var comp = RK.comp();
                if (!comp) return false;
                RK.undo(RK.undoName("Controllers"), function () { RK.cp.create(comp); });
                return true;
            }, true)
        }
    ]);
};
// The tool opens its page through run() (Null-type controllers skip the page).
RK.tools.controllers.page = null;

// ============================================================================ Rigging tools
//
// Rot Morph, Bones, List, Rename, Measure, Replace and Lock property. (IK, IK Goal, Bezier IK,
// Controllers, Zero and the Auto-Rig are in 50_rigging.js.) Each button only gathers its input;
// the work is done by plain functions (RK.rt.*) that tests can call with options.

RK.rt = {};

// ---------------------------------------------------------------------------- small helpers

// An expression string literal in single quotes (RK.q gives the double-quoted one).
RK.rt.q1 = function (s) {
    return "'" + String(s).replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'";
};

RK.rt.escapeRe = function (s) {
    return String(s).replace(/[.*+?^${}()|[\]\\\/]/g, "\\$&");
};

// Every occurrence of `find` in s replaced by `by`, literally (no patterns, no "$" codes).
RK.rt.replaceAll = function (s, find, by, caseSensitive) {
    s = String(s);
    if (!find) return s;
    if (caseSensitive !== false) return s.split(find).join(by);
    return s.replace(new RegExp(RK.rt.escapeRe(find), "gi"), function () { return by; });
};

// Fast expression reads (RK.xFast) and the conversions it doesn't have: rkC / rkFC are
// toComp / fromComp, rkFW fromWorld, all on a transform from rkXf(); rkMe is this layer.
RK.rt.XFAST = RK.xFast +
    "function rkC(X, p) { return rkF ? __m3(X[0], p) : X.toComp(p); }\n" +
    "function rkFC(X, p) { return rkF ? __m3(X[1], p) : X.fromComp(p); }\n" +
    "function rkFW(X, p) { return rkF ? __m4(X[3], p, false) : X.fromWorld(p); }\n" +
    "var rkMe = rkF ? thisLayer.__l : thisLayer;\n";

// `var v = rk(...)`, null while a value it depends on (dep) is still missing.
RK.rt.read = function (v, dep, code) {
    return "var " + v + " = " + (dep ? dep + " == null ? null : " : "") + "rk(function () { return " + code + "; });\n";
};
RK.rt.readLayer = function (v, name) {
    return RK.rt.read(v, null, "rkLayer(" + RK.q(name) + ")");
};
// The first parameter of effect `effectName` on layer variable L ("rkMe": this layer). Names
// the host can't take as a path segment ("/", ".", "#n", "@n") go through the object model.
RK.rt.readFx = function (v, L, effectName) {
    var path = /[#@]/.test(effectName) ? "" : RK.fxPath(effectName);
    return RK.rt.read(v, L === "rkMe" ? null : L, "rkVal(" + L + ", " + RK.q(path) + ", function (L) { return L.effect(" + RK.q(effectName) + ")(1).value; })");
};
// A transform value (match id: position, scale, anchor, rotation, orientation, rotationX...).
RK.rt.readTr = function (v, L, id, api) {
    return RK.rt.read(v, L === "rkMe" ? null : L, "rkVal(" + L + ", \"transform/" + id + "\", function (L) { return L.transform." + api + ".value; })");
};
RK.rt.readXf = function (v, L) {
    return RK.rt.read(v, L === "rkMe" ? null : L, "rkXf(" + L + ")");
};
// The parent's transform: false without a parent.
RK.rt.readParentXf = function (v, L) {
    return RK.rt.read(v + "P", L === "rkMe" ? null : L, "rkParent(" + L + ")") +
        "var " + v + " = " + v + "P == null ? null : (" + v + "P === false ? false : rk(function () { return rkXf(" + v + "P); }));\n";
};

// Every composition of the project.
RK.rt.allComps = function () {
    var out = [], n = app.project.numItems;
    for (var i = 1; i <= n; i++) {
        var it = app.project.item(i);
        if (it instanceof CompItem) out.push(it);
    }
    return out;
};

// Adding an effect moves EffectCraft's property selection to the new effect (while the old
// property's keys stay selected): select the rigged property again, as the user left it.
RK.rt.reselect = function (prop) {
    try {
        var L = RK.layerOf(prop);
        app.run("prop.select", { comp: L.containingComp.id, layer: L.id, prop: prop.__uid });
    } catch (e) {}
};

RK.rt.layersOf = function (comp) {
    var out = [];
    for (var i = 1; i <= comp.numLayers; i++) out.push(comp.layer(i));
    return out;
};

// The comp-space (2D) or world (3D) position of a layer's anchor point, now. Cameras and lights
// have no anchor: their own position. 2D layers are read without touching the project; the
// others go through a temporary probe (call inside an undo group).
RK.rt.worldPos = function (layer) {
    if (layer instanceof AVLayer && !RK.is3D(layer) && !RK.hasParent3D(layer)) {
        var w = RK.world(layer);
        return [w.pos[0], w.pos[1], 0];
    }
    return RK.rt.probeWorld(layer);
};
RK.rt.needsProbe = function (layer) {
    return !(layer instanceof AVLayer && !RK.is3D(layer) && !RK.hasParent3D(layer));
};
RK.rt.probeWorld = function (layer) {
    var a = layer instanceof CameraLayer || layer instanceof LightLayer ? "[0, 0, 0]" : "L.transform.anchorPoint.valueAtTime(T)";
    var r = RK.probe(layer, "var p = L.toWorld(" + a + ", T);\n[p[0], p[1], p.length > 2 ? p[2] : 0, 0]");
    return [r[0], r[1], r[2]];
};

// ---------------------------------------------------------------------------- Rot Morph
//
// The property replays its own keyframed animation as the reference layer turns: the reference
// rotation going from Min. Angle to Max. Angle scrubs the property from the comp start to its
// last keyframe.

RK.rt.rotMorph = function (prop) {
    var layer = RK.layerOf(prop), path = RK.propPath(prop);
    var name = RK.uniqueEffectName(layer, prop.name + " Rotation Morph");
    RK.addGroup(layer, name, [
        { name: "Reference Layer", type: "layer", value: layer },
        { name: "Min. Angle", type: "angle", value: 0 },
        { name: "Max. Angle", type: "angle", value: 0 }
    ]);
    prop = RK.propAt(layer, path);
    RK.setExpr(prop, RK.rt.rotMorphExpr(name));
    RK.rt.reselect(prop);
    return { prop: prop, group: name };
};

RK.rt.rotMorphExpr = function (group) {
    var x = RK.xHead("rotation morph");
    x += RK.xGet("R", "", RK.xfx("", group, "Reference Layer") + ".value");
    x += RK.xGet("a0", "", RK.xfx("", group, "Min. Angle") + ".value");
    x += RK.xGet("a1", "", RK.xfx("", group, "Max. Angle") + ".value");
    x += RK.xGet("n", "", "numKeys");
    x += "var t1 = n >= 2 ? rk(function () { return key(n).time; }) : 0;\n";
    x += RK.xGet("r", "R", "R.transform.rotation.value");
    x += "if (rk_wait || !R || !(n >= 2)) { value; } else {\n";
    x += "    var u = a1 == a0 ? 0 : (r - a0) / (a1 - a0);\n";
    x += "    valueAtTime(u * t1);\n";
    x += "}\n";
    return x;
};

RK.tool("rotmorph", {
    label: "Rot Morph", icon: RK.I.rotmorph,
    tip: "Rotation Morph: the selected property replays its keyframed animation as a layer rotates (set the Reference Layer and the Min. / Max. Angle in its effects).",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var p = RK.selectedProperty(comp);
        if (!p) return;
        RK.undo(RK.NAME + " - Rotation Morph", function () { RK.rt.rotMorph(p); });
    }
});

// ---------------------------------------------------------------------------- List
//
// Five weighted slots summed on the property: result = sum of Value k * Weight k / 100. Slot 1
// starts with the property's value (and takes over its expression), so nothing moves.

RK.rt.list = function (prop) {
    var t = prop.propertyValueType, T = PropertyValueType;
    var numeric = t === T.OneD || t === T.TwoD || t === T.TwoD_SPATIAL || t === T.ThreeD || t === T.ThreeD_SPATIAL;
    var d = RK.dims(prop);
    if (!numeric || d > 3) throw new Error("only a property with one, two or three numbers can be listed (not a colour, a path or a text).");
    var layer = RK.layerOf(prop), path = RK.propPath(prop);
    var comp = layer.containingComp;
    var oldExpr = "";
    try {
        oldExpr = prop.expression || "";
    } catch (e) {}
    if (!/\S/.test(oldExpr)) oldExpr = "";
    // With an expression moving into slot 1, slot 1 keeps the static value the expression
    // worked from; without one it holds what the property shows now.
    var v = prop.valueAtTime(comp.time, !!oldExpr);
    var vz = d === 3 ? prop.valueAtTime(comp.time, false)[2] : 0;
    var name = RK.uniqueEffectName(layer, prop.name + " List");
    var params = [];
    for (var k = 1; k <= 5; k++) {
        if (d === 1) params.push({ name: k + " Value", type: "slider", value: k === 1 ? (v instanceof Array ? v[0] : v) : 0 });
        else params.push({ name: k + " Value", type: "point", value: k === 1 ? [v[0], v[1]] : [0, 0] });
        if (d === 3) params.push({ name: k + " Z Value", type: "slider", value: k === 1 ? vz : 0 });
        params.push({ name: k + " Weight", type: "slider", value: 100 });
    }
    var g = RK.addGroup(layer, name, params);
    if (oldExpr) g.fx["1 Value"].property(1).expression = oldExpr;
    prop = RK.propAt(layer, path);
    RK.setExpr(prop, RK.rt.listExpr(name, d));
    RK.rt.reselect(prop);
    return { prop: prop, group: name, dims: d };
};

RK.rt.listExpr = function (group, d) {
    var x = RK.xHead("list") + RK.rt.XFAST, terms = [];
    for (var k = 1; k <= 5; k++) {
        x += RK.rt.readFx("v" + k, "rkMe", RK.fxg(group, k + " Value"));
        if (d === 3) x += RK.rt.readFx("z" + k, "rkMe", RK.fxg(group, k + " Z Value"));
        x += RK.rt.readFx("w" + k, "rkMe", RK.fxg(group, k + " Weight"));
        if (d === 1) terms.push("v" + k + " * w" + k + " / 100");
        else if (d === 2) terms.push("[v" + k + "[0], v" + k + "[1]] * (w" + k + " / 100)");
        else terms.push("[v" + k + "[0], v" + k + "[1], z" + k + "] * (w" + k + " / 100)");
    }
    x += "if (rk_wait) { value; } else {\n    " + terms.join(" +\n    ") + ";\n}\n";
    return x;
};

RK.tool("list", {
    label: "List", icon: RK.I.list,
    tip: "Adds a list of five weighted slots on the selected property: the property becomes their weighted sum (its own keyframes are then ignored).",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var p = RK.selectedProperty(comp);
        if (!p) return;
        RK.undo(RK.NAME + " - List", function () { RK.rt.list(p); });
    }
});

// ---------------------------------------------------------------------------- Lock property

RK.rt.numText = function (x) {
    if (typeof x !== "number" || !isFinite(x)) throw new Error("the value isn't a finite number.");
    var s = String(x);
    return s === "-0" ? "0" : s;
};

// A value as constant expression text, in full precision.
RK.rt.constText = function (v) {
    var i;
    if (typeof v === "number") return RK.rt.numText(v);
    if (typeof v === "boolean") return v ? "1" : "0";
    if (typeof v === "string") return JSON.stringify(v);
    if (v instanceof Array) {
        var o = [];
        for (i = 0; i < v.length; i++) o.push(RK.rt.numText(v[i]));
        return "[" + o.join(", ") + "]";
    }
    if (v && v.vertices instanceof Array) {
        var pts = function (a) {
            var out = [];
            for (var j = 0; j < (a ? a.length : 0); j++) out.push("[" + RK.rt.numText(a[j][0]) + ", " + RK.rt.numText(a[j][1]) + "]");
            return "[" + out.join(", ") + "]";
        };
        return "createPath(" + pts(v.vertices) + ", " + pts(v.inTangents) + ", " + pts(v.outTangents) + ", " + (v.closed !== false ? "true" : "false") + ")";
    }
    if (v && typeof v.text === "string") return JSON.stringify(v.text);
    throw new Error("this kind of value can't be locked.");
};

RK.rt.lockProp = function (prop) {
    var comp = RK.layerOf(prop).containingComp;
    var v = prop.valueAtTime(comp.time, false);
    RK.setExpr(prop, RK.TAG + "lock property\n" + RK.rt.constText(v));
    return v;
};

RK.tool("lock", {
    label: "Lock property", icon: RK.I.lock,
    tip: "Locks the selected property to its current value with an expression (delete the expression to unlock it).",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var p = RK.selectedProperty(comp);
        if (!p) return;
        RK.undo(RK.NAME + " - Lock", function () { RK.rt.lockProp(p); });
    }
});

// ---------------------------------------------------------------------------- Bones
//
// A bone is a small guide solid (or null) at a point of a layer (a puppet pin, an effect point,
// a position...); the point follows the bone through an expression, so the bone can be
// parented and rigged like any layer.

RK.rt.boneSettings = function () {
    return {
        type: RK.pref("boneType", "solid"),
        size: RK.pref("boneSize", 20),
        autoSize: RK.pref("boneAutoSize", true),
        sizeHint: RK.pref("boneSizeHint", "medium"),
        color: RK.pref("boneColor", "FF0000"),
        placement: RK.pref("bonePlacement", "over")
    };
};

RK.rt.hexColor = function (hex) {
    var s = String(hex === undefined || hex === null ? "" : hex).replace(/^\s*#?|\s+$/g, "");
    if (!/^[0-9a-fA-F]{6}$/.test(s)) s = "FF0000";
    var n = parseInt(s, 16);
    return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
};

RK.rt.isPin = function (p) {
    if (!p || !(p instanceof PropertyGroup)) return false;
    var m = String(p.matchName);
    return m === "pin" || /PosPin Atom$/.test(m);
};

// A pin's Position (Bend pins have none).
RK.rt.pinPosition = function (pin) {
    for (var i = 1; i <= pin.numProperties; i++) {
        var c = pin.property(i);
        if (c instanceof Property && (c.matchName === "position" || /PosPin Position$/.test(c.matchName))) return c;
    }
    return null;
};

RK.rt.isSpatial = function (p) {
    if (!(p instanceof Property)) return false;
    var t = p.propertyValueType;
    return t === PropertyValueType.TwoD_SPATIAL || t === PropertyValueType.ThreeD_SPATIAL;
};

RK.rt.insideEffect = function (p) {
    var q = p;
    while (q && !(q instanceof Layer)) {
        if (q.matchName === "ADBE Effect Parade") return true;
        q = q.parentProperty;
    }
    return false;
};

// Where a point property's values live: "effect" (effect points, puppet pins: the layer's
// effect space), "parent" (Transform > Position, a camera's or light's Point of Interest: the
// parent's space) or "layer" (anything else: the layer's own space).
RK.rt.pointSpace = function (prop) {
    if (RK.rt.insideEffect(prop)) return "effect";
    var parent = prop.parentProperty, layer = RK.layerOf(prop);
    var inTransform = parent && parent.matchName === "ADBE Transform Group";
    if (inTransform && prop.matchName === "ADBE Position") return "parent";
    if (inTransform && prop.matchName === "ADBE Anchor Point" && (layer instanceof CameraLayer || layer instanceof LightLayer)) return "parent";
    return "layer";
};

// All the puppet pins of a layer, in the order EffectCraft lists them.
RK.rt.allPins = function (layer) {
    var out = [], fx = RK.effects(layer);
    if (!fx) return out;
    var walk = function (g, depth) {
        if (depth > 6) return;
        for (var i = 1; i <= g.numProperties; i++) {
            var c = g.property(i);
            if (!(c instanceof PropertyGroup)) continue;
            if (RK.rt.isPin(c)) out.push(c);
            else walk(c, depth + 1);
        }
    };
    for (var i = 1; i <= fx.numProperties; i++) {
        var e = fx.property(i);
        if (e.matchName === "ADBE FreePin3") walk(e, 0);
    }
    return out;
};

// The points of a layer to put bones on: its selected puppet pins, else its selected spatial
// properties, else all its puppet pins. Each: {prop (the point), name (the bone's base name)}.
RK.rt.bonePoints = function (layer) {
    var sel = layer.selectedProperties, pins = [], props = [], seen = {}, i, p;
    var addPin = function (pin) {
        var pp = RK.rt.pinPosition(pin);
        if (!pp || seen[pp.__uid]) return;
        seen[pp.__uid] = true;
        pins.push({ prop: pp, name: "B_" + pin.name });
    };
    for (i = 0; i < sel.length; i++) {
        p = sel[i];
        if (RK.rt.isPin(p)) addPin(p);
        else if (p instanceof Property && RK.rt.isPin(p.parentProperty) && RK.rt.pinPosition(p.parentProperty)) addPin(p.parentProperty);
    }
    if (pins.length) return pins;
    for (i = 0; i < sel.length; i++) {
        p = sel[i];
        if (!RK.rt.isSpatial(p) || !RK.canExpr(p) || seen[p.__uid]) continue;
        seen[p.__uid] = true;
        var par = p.parentProperty;
        var n = "B_" + p.name + "_" + (par && !(par instanceof Layer) ? par.name : layer.name);
        if (n.length > 32) n = n.substr(0, 32);
        props.push({ prop: p, name: n });
    }
    if (props.length) return props;
    var all = RK.rt.allPins(layer);
    for (i = 0; i < all.length; i++) addPin(all[i]);
    return pins;
};

// Bone size in pixels for a layer (Settings > Rigging > Bones).
RK.rt.boneSize = function (comp, layer, st) {
    st = st || RK.rt.boneSettings();
    if (!st.autoSize) return Math.max(1, Math.round(Number(st.size) || 20));
    var div = st.sizeHint === "small" ? 60 : st.sizeHint === "big" ? 20 : 40;
    var base;
    if (layer instanceof ShapeLayer || layer instanceof TextLayer || !(layer.width > 0 && layer.height > 0)) {
        base = comp.width / 2 + comp.height / 2;
    } else {
        // the layer's size on screen: its source size times its world scale (in %), / 130
        var w = RK.world(layer);
        base = (layer.width * Math.abs(w.sx) * 100 + layer.height * Math.abs(w.sy) * 100) / 130;
    }
    return Math.max(4, Math.floor(base / div));
};

// The comp position of a point now, from its value and its space.
RK.rt.pointComp = function (layer, prop, space) {
    var v = prop.valueAtTime(layer.containingComp.time, false);
    if (space === "parent") {
        if (!layer.parent) return [v[0], v[1]];
        return RK.compPoint(layer.parent, [v[0], v[1], v.length > 2 ? v[2] : 0]);
    }
    var off = space === "effect" ? RK.effectOffset(layer) : [0, 0];
    return RK.compPoint(layer, [v[0] - off[0], v[1] - off[1], 0]);
};

// The point's expression: the bone's anchor point in the comp, back in the point's space.
// (The bone's anchor is written in: bones keep their anchor at their centre.)
RK.rt.boneExpr = function (boneName, anchor, space, shapeLike) {
    var x = RK.xHead("bones") + RK.rt.XFAST;
    var a = RK.xValue([anchor[0], anchor[1]]);
    x += RK.rt.readLayer("B", boneName);
    x += RK.rt.readXf("bX", "B");
    if (space === "parent") {
        x += RK.rt.readParentXf("pX", "rkMe");
        x += "if (rk_wait) { value; } else { var w = rkC(bX, " + a + "); var p = pX === false ? w : rkFC(pX, w); [p[0], p[1]]; }\n";
        return x;
    }
    x += RK.rt.readXf("mX", "rkMe");
    if (space === "effect" && shapeLike) {
        x += RK.rt.read("o", null, "[thisComp.width / 2, thisComp.height / 2]");
        x += "if (rk_wait) { value; } else { var p = rkFC(mX, rkC(bX, " + a + ")); [p[0] + o[0], p[1] + o[1]]; }\n";
    } else {
        x += "if (rk_wait) { value; } else { var p = rkFC(mX, rkC(bX, " + a + ")); [p[0], p[1]]; }\n";
    }
    return x;
};

// One bone for one point. Returns the bone layer.
RK.rt.makeBone = function (comp, layer, point, size, st, names) {
    var prop = point.prop, space = RK.rt.pointSpace(prop);
    var at = RK.rt.pointComp(layer, prop, space);
    var name = RK.uniqueName(comp, point.name, names);
    var b;
    if (st.type === "null") {
        b = comp.layers.addNull();
    } else {
        b = comp.layers.addSolid(RK.rt.hexColor(st.color), name, size, size, comp.pixelAspect || 1);
    }
    b.name = name;
    try {
        if (st.placement === "over") b.moveBefore(layer);
        else if (st.placement === "under") b.moveAfter(layer);
        else if (st.placement === "bottom") b.moveToEnd();
    } catch (e) {}
    RK.addTag(b, RK.BONE_TAG);
    b.transform.position.setValue([at[0], at[1], 0]);
    b.guideLayer = true;
    var a = b.transform.anchorPoint.value;
    var shapeLike = layer instanceof ShapeLayer || layer instanceof TextLayer;
    RK.setExpr(prop, RK.rt.boneExpr(name, a, space, shapeLike));
    return b;
};

// Bones for the selected layers' points. Returns the bones made; stops (with a message) at the
// first layer that has no point to rig.
RK.rt.bones = function (comp, layers) {
    layers = layers || comp.selectedLayers;
    if (!layers.length) {
        RK.msg("Select a puppet pin or a spatial property (an effect point, a position...) to create a bone.");
        return [];
    }
    // read every layer's points before adding layers changes the selection
    var plan = [], i, j;
    for (i = 0; i < layers.length; i++) plan.push({ layer: layers[i], points: RK.rt.bonePoints(layers[i]) });
    var st = RK.rt.boneSettings();
    RK.nameCheck(comp);
    var names = RK.layerNames(comp), made = [];
    for (i = 0; i < plan.length; i++) {
        var L = plan[i].layer, pts = plan[i].points;
        if (!pts.length) {
            RK.msg("\"" + L.name + "\" has no puppet pin and no selected spatial property. Select a property with two or three dimensions (an effect point, a position...) to link it to a bone; other properties can't be linked.");
            break;
        }
        var size = RK.rt.boneSize(comp, L, st);
        for (j = 0; j < pts.length; j++) made.push(RK.rt.makeBone(comp, L, pts[j], size, st, names));
    }
    return made;
};

RK.tool("bones", {
    label: "Bones", icon: RK.I.bones,
    tip: "Creates a bone on each selected puppet pin or point (effect point, position...), or on every pin of the selected layers; the point then follows its bone.",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        RK.undo(RK.NAME + " - Bone", function () { RK.rt.bones(comp); });
    }
});

// ---------------------------------------------------------------------------- Measure

RK.rt.measureText = "Distance =\npixels";

// Distance between the anchor points of two layers (3D when they have depth), rounded to the
// pixel. Also fills the Wheel tool's radius. Returns the distance, or null.
RK.rt.measure = function (comp, layers) {
    layers = layers || comp.selectedLayers;
    if (layers.length !== 2) {
        RK.msg("Select two layers to measure the distance between them.");
        return null;
    }
    var get = function () { return [RK.rt.worldPos(layers[0]), RK.rt.worldPos(layers[1])]; };
    var p = RK.rt.needsProbe(layers[0]) || RK.rt.needsProbe(layers[1]) ? RK.undo(RK.NAME + " - Measure", get) : get();
    var d = Math.round(RK.dist(p[0], p[1]));
    RK.shared.wheelRadius = d;
    RK.rt.measureText = d === 0 ? "The two layers are\nat the same place." : "Distance =\n" + d + " pixels.";
    return d;
};

RK.tool("measure", {
    label: "Measure", icon: RK.I.measure,
    tip: "Measures the distance between the two selected layers (also used as the Wheel tool's radius).",
    page: function (body) {
        var comp = RK.comp();
        if (comp) RK.rt.measure(comp);
        var lines = RK.rt.measureText.split("\n");
        var t = body.add("statictext", undefined, RK.rt.measureText);
        t.preferredSize = [RK.PAGE_W, 18 * lines.length + 4];
        t.onDraw = function () {
            var g = this.graphics;
            for (var i = 0; i < lines.length; i++) {
                g.drawString(lines[i], null, Math.round((RK.PAGE_W - RK.textWidth(lines[i], RK.FONT)) / 2), 2 + 18 * i);
            }
        };
        RK.ui.bottomRow(body, [RK.ui.backItem()]);
    }
});

// ---------------------------------------------------------------------------- expression edits
//
// Rename and Replace rewrite expressions as text. Each layer's expressions are read in one call
// (layer.tree); the changed ones are written back in one batch, as one step.

// Calls fn(text, layer) for every expression of the layers (all of the comp's when null);
// writes back the texts it changed. Returns the number of expressions changed.
RK.rt.editExpressions = function (comp, layers, fn) {
    layers = layers || RK.rt.layersOf(comp);
    var steps = [];
    for (var i = 0; i < layers.length; i++) {
        var L = layers[i], tree;
        try {
            tree = app.run("layer.tree", { comp: comp.id, layer: L.id, depth: 40 });
        } catch (e) {
            continue;
        }
        RK.rt.collectEdits(comp, L, tree.properties, [], fn, steps);
    }
    if (!steps.length) return 0;
    app.run("engine.batch", { label: RK.NAME + " - expressions", atomic: false, steps: steps });
    return steps.length;
};

RK.rt.collectEdits = function (comp, layer, node, path, fn, steps) {
    if (!node) return;
    if (typeof node.expression === "string" && node.expression !== "") {
        var t = fn(node.expression, layer);
        if (t !== node.expression) {
            var params = { comp: comp.id, layer: layer.id, prop: node.uid, expression: t };
            // keep a switched-off expression switched off
            try {
                var p = RK.propAt(layer, path);
                if (p && p.__uid === node.uid && !p.expressionEnabled) params.enabled = false;
            } catch (e) {}
            steps.push({ command: "prop.setExpression", params: params });
        }
    }
    var ch = node.children;
    if (!ch) return;
    for (var i = 0; i < ch.length; i++) RK.rt.collectEdits(comp, layer, ch[i], path.concat([i + 1]), fn, steps);
};

// The comp named by a `comp("name").` reference ending right where `before` ends, or null.
RK.rt.compRefBefore = function (before) {
    var m = /comp\(\s*(["'])((?:\\.|(?!\1)[^\\])*)\1\s*\)\s*\.\s*$/.exec(before);
    if (!m) return null;
    if (/[A-Za-z0-9_$]$/.test(before.substr(0, m.index))) return null;
    return m[2].replace(/\\(.)/g, "$1");
};

// Rename the references to a layer in one expression. `inComp`: the expression is in the
// renamed layer's comp (so `thisComp.layer(...)` and `layer(...)` mean that comp); `compName`:
// that comp's name (`comp("compName").layer(...)` from anywhere).
RK.rt.renameLayerRefs = function (text, oldName, newName, compName, inComp) {
    var forms = [[RK.q(oldName), RK.q(newName)], [RK.rt.q1(oldName), RK.rt.q1(newName)]];
    var f;
    // Rig Kit's own fast reads, rkLayer("name") (RK.xFast), always mean this comp
    if (inComp) {
        for (f = 0; f < forms.length; f++) text = text.split("rkLayer(" + forms[f][0]).join("rkLayer(" + forms[f][1]);
    }
    for (f = 0; f < forms.length; f++) {
        var needle = "layer(" + forms[f][0], out = "", pos = 0, i;
        while ((i = text.indexOf(needle, pos)) >= 0) {
            var before = text.substring(0, i);
            var ok = !/[A-Za-z0-9_$]$/.test(before);
            if (ok) {
                var ref = RK.rt.compRefBefore(before);
                ok = ref === null ? inComp : ref === compName;
            }
            out += text.substring(pos, i) + (ok ? "layer(" + forms[f][1] : needle);
            pos = i + needle.length;
        }
        text = out + text.substring(pos);
    }
    return text;
};

// Rename the references to a comp: comp("old") -> comp("new").
RK.rt.renameCompRefs = function (text, oldName, newName) {
    var forms = [[RK.q(oldName), RK.q(newName)], [RK.rt.q1(oldName), RK.rt.q1(newName)]];
    for (var f = 0; f < forms.length; f++) {
        var needle = "comp(" + forms[f][0], out = "", pos = 0, i;
        while ((i = text.indexOf(needle, pos)) >= 0) {
            var ok = !/[A-Za-z0-9_$]$/.test(text.substring(0, i));
            out += text.substring(pos, i) + (ok ? "comp(" + forms[f][1] : needle);
            pos = i + needle.length;
        }
        text = out + text.substring(pos);
    }
    return text;
};

// After layers were renamed (renames: [{comp, compName, old, name}], in the order they were
// renamed): update the expressions of their comps, and with `project` the
// comp("...").layer("...") references in every comp. One pass over the expressions applies
// the renames in order, as renaming them one by one would.
RK.rt.updateLayerRefs = function (renames, project) {
    if (!renames.length) return 0;
    var comps = [], seen = {}, i, n = 0;
    if (project) comps = RK.rt.allComps();
    else {
        for (i = 0; i < renames.length; i++) {
            var C = renames[i].comp;
            if (!seen[C.id]) {
                seen[C.id] = true;
                comps.push(C);
            }
        }
    }
    var edit = function (C) {
        // Rig Kit's rig expressions name their layers by id and keep the names aside
        var byId = {};
        for (var j = 0; j < renames.length; j++) if (renames[j].comp.id === C.id && renames[j].id !== undefined) byId[renames[j].id] = renames[j].name;
        return RK.rt.editExpressions(C, null, function (t) {
            if (RK.rkx && RK.rkx.isRkx(t)) return RK.rkx.renameIn(t, byId);
            for (var k = 0; k < renames.length; k++) {
                var r = renames[k];
                t = RK.rt.renameLayerRefs(t, r.old, r.name, r.compName, C.id === r.comp.id);
            }
            return t;
        });
    };
    for (i = 0; i < comps.length; i++) n += edit(comps[i]);
    return n;
};

// After comps were renamed (renames: [{old, name}], in order): comp("old") -> comp("new") in
// every expression of the project.
RK.rt.updateCompRefs = function (renames) {
    if (!renames.length) return 0;
    var comps = RK.rt.allComps(), n = 0;
    var fn = function (t) {
        for (var k = 0; k < renames.length; k++) t = RK.rt.renameCompRefs(t, renames[k].old, renames[k].name);
        return t;
    };
    for (var i = 0; i < comps.length; i++) n += RK.rt.editExpressions(comps[i], null, fn);
    return n;
};

// Rename a layer; returns what updateLayerRefs needs, or null when the name doesn't change
// (an empty name leaves the layer as it is).
RK.rt.renameLayer = function (L, newName) {
    var old = L.name;
    if (!newName || newName === old) return null;
    RK.withUnlocked(L, function () { L.name = newName; });
    var comp = L.containingComp;
    return { comp: comp, compName: comp.name, old: old, name: newName, id: L.id };
};

RK.rt.renameLayers = function (layers, names, update, project) {
    var done = [];
    for (var i = 0; i < layers.length; i++) {
        var r = RK.rt.renameLayer(layers[i], names[i]);
        if (r) done.push(r);
    }
    if (update) RK.rt.updateLayerRefs(done, project);
    return done.length;
};

// Rename project items; comps' references are updated when `update` is on.
RK.rt.renameItems = function (items, names, update) {
    var comps = [], n = 0;
    for (var i = 0; i < items.length; i++) {
        var it = items[i], old = it.name;
        if (!names[i] || names[i] === old) continue;
        it.name = names[i];
        n++;
        if (it instanceof CompItem) comps.push({ old: old, name: names[i] });
    }
    if (update) RK.rt.updateCompRefs(comps);
    return n;
};

// ---------------------------------------------------------------------------- Rename

// The new name of an object. o: {useName, name, usePrefix, prefix, useSuffix, suffix,
// removeFirst, removeLast, useNumber, numberFrom}; i: the object's number in the run.
RK.rt.newName = function (old, o, i) {
    var base;
    if (o.useName) {
        base = o.name || "";
    } else {
        base = String(old);
        if (o.removeFirst > 0) base = base.substr(Math.min(o.removeFirst, base.length));
        if (o.removeLast > 0) base = base.substr(0, Math.max(0, base.length - o.removeLast));
    }
    var n = (o.usePrefix ? o.prefix || "" : "") + base + (o.useSuffix ? o.suffix || "" : "");
    if (o.useNumber) n += String(o.numberFrom + i);
    return n;
};

// o: the newName options plus {mode: "layers"|"pins"|"items", update, project}.
// Returns the number of objects renamed (or -1 after a message).
RK.rt.rename = function (o) {
    var i, n = 0;
    if (o.useNumber && !(typeof o.numberFrom === "number" && isFinite(o.numberFrom))) {
        RK.msg("Number from: type a whole number.");
        return -1;
    }
    if (o.mode === "items") {
        var items = app.project.selection;
        if (!items.length) {
            RK.msg("No item is selected. Select project items to rename.");
            return -1;
        }
        var inames = [];
        for (i = 0; i < items.length; i++) inames.push(RK.rt.newName(items[i].name, o, i));
        RK.undo(RK.NAME + " - Rename", function () { n = RK.rt.renameItems(items, inames, o.update); });
        return n;
    }
    var comp = RK.comp();
    if (!comp) return -1;
    var layers = comp.selectedLayers;
    if (!layers.length) {
        RK.msg("No layer is selected. Select the layers" + (o.mode === "pins" ? " with the pins" : "") + " to rename.");
        return -1;
    }
    RK.undo(RK.NAME + " - Rename", function () {
        if (o.mode === "pins") {
            var k = 0;
            for (i = 0; i < layers.length; i++) {
                var sel = layers[i].selectedProperties, pins = [];
                for (var j = 0; j < sel.length; j++) if (RK.rt.isPin(sel[j])) pins.push(sel[j]);
                if (!pins.length) pins = RK.rt.allPins(layers[i]);
                for (j = 0; j < pins.length; j++) {
                    var nn = RK.rt.newName(pins[j].name, o, k++);
                    if (nn && nn !== pins[j].name) {
                        pins[j].name = nn;
                        n++;
                    }
                }
            }
            return;
        }
        var names = [];
        for (i = 0; i < layers.length; i++) names.push(RK.rt.newName(layers[i].name, o, i));
        n = RK.rt.renameLayers(layers, names, o.update, o.project);
    });
    return n;
};

RK.rt.renameState = {
    mode: 0, useName: false, name: "", usePrefix: false, prefix: "", useSuffix: false, suffix: "",
    removeFirst: "0", removeLast: "0", useNumber: false, numberFrom: "1", update: true, scope: 0
};

// A row: a check-box (fixed width) and a text field it enables.
RK.rt.checkField = function (parent, label, on, text, labelW, number) {
    var r = RK.ui.row(parent);
    // a fixed-width cell keeps the check-boxes of several rows lined up on the left
    var cell = r.add("group");
    cell.orientation = "row";
    cell.alignChildren = ["left", "center"];
    cell.margins = [0, 0, 0, 0];
    cell.preferredSize = [labelW, 22];
    var c = RK.ui.check(cell, label, on);
    var w = RK.PAGE_W - labelW - 4;
    var e = number ? RK.ui.number(r, text, 50) : RK.ui.text(r, text, w);
    e.enabled = !!on;
    c.onClick = function () { e.enabled = this.value; };
    r.check = c;
    r.field = e;
    return r;
};

RK.rt.renamePage = function (body) {
    var st = RK.rt.renameState, LW = 104;
    var mode = RK.ui.radios(body, ["Layers", "Pins", RK.ui.expert ? "Items" : "Project Items"], st.mode, { onChange: function () { sync(); } });
    RK.ui.separator(body);
    var fName = RK.rt.checkField(body, "Name", st.useName, st.name, LW);
    var fPre = RK.rt.checkField(body, "Prefix", st.usePrefix, st.prefix, LW);
    var fSuf = RK.rt.checkField(body, "Suffix", st.useSuffix, st.suffix, LW);
    var r1 = RK.ui.row(body, "Remove first", LW - 4);
    var rFirst = RK.ui.number(r1, st.removeFirst, 50, "Characters removed from the start of the name");
    RK.ui.label(r1, "digits.", 60);
    var r2 = RK.ui.row(body, "Remove last", LW - 4);
    var rLast = RK.ui.number(r2, st.removeLast, 50, "Characters removed from the end of the name");
    RK.ui.label(r2, "digits.", 60);
    var fNum = RK.rt.checkField(body, "Number from", st.useNumber, st.numberFrom, LW, true);
    RK.ui.separator(body);
    var upd = RK.ui.check(body, "Update expressions", st.update, "Update the expressions that refer to the renamed layers or comps");
    var scope = RK.ui.radios(body, ["Current comp", "Project"], st.scope);
    var sync = function () {
        var m = mode.getValue();
        upd.enabled = m !== 1;
        scope.setEnabled(m === 0 && upd.value);
        if (m === 2) scope.setValue(0);
    };
    upd.onClick = sync;
    sync();
    var save = function () {
        st.mode = mode.getValue();
        st.useName = fName.check.value; st.name = fName.field.text;
        st.usePrefix = fPre.check.value; st.prefix = fPre.field.text;
        st.useSuffix = fSuf.check.value; st.suffix = fSuf.field.text;
        st.removeFirst = rFirst.text; st.removeLast = rLast.text;
        st.useNumber = fNum.check.value; st.numberFrom = fNum.field.text;
        st.update = upd.value; st.scope = scope.getValue();
    };
    RK.ui.bottomRow(body, [RK.ui.backItem(), {
        label: "Rename", icon: RK.I.ok, tip: "Rename",
        onClick: RK.ui.action("Rename", function () {
            save();
            var m = st.mode;
            var num = parseInt(String(st.numberFrom).replace(/^\s+|\s+$/g, ""), 10);
            RK.rt.rename({
                mode: m === 1 ? "pins" : m === 2 ? "items" : "layers",
                useName: st.useName, name: st.name, usePrefix: st.usePrefix, prefix: st.prefix,
                useSuffix: st.useSuffix, suffix: st.suffix,
                removeFirst: RK.ui.int(rFirst, 0, 0), removeLast: RK.ui.int(rLast, 0, 0),
                useNumber: st.useNumber, numberFrom: /^\s*-?\d+\s*$/.test(st.numberFrom) ? num : NaN,
                update: m !== 1 && st.update, project: m === 0 && st.scope === 1
            });
            rFirst.text = "0";
            rLast.text = "0";
            st.removeFirst = "0";
            st.removeLast = "0";
        })
    }]);
};

RK.tool("rename", {
    label: "Rename", icon: RK.I.rename,
    tip: "Renames the selected layers, puppet pins or project items, and updates the expressions that use their names.",
    page: function (body) { RK.rt.renamePage(body); }
});

// ---------------------------------------------------------------------------- Replace

// o: {mode: "expressions"|"layers"|"items", search, replace, caseSensitive,
//     allComps, selectedLayers (Expressions/Layers in the active comp),
//     comps, footage, folders, selectedItems (Items), update, project}
// Returns the number of changes (or -1 after a message).
RK.rt.replace = function (o) {
    var search = String(o.search || ""), by = String(o.replace || "");
    if (!search || search === by) return 0;
    var cs = o.caseSensitive !== false, n = 0, i;
    var rep = function (s) { return RK.rt.replaceAll(s, search, by, cs); };
    if (o.mode === "items") {
        var items = [];
        if (o.selectedItems) {
            items = app.project.selection;
            if (!items.length) {
                RK.msg("No item is selected. Select project items.");
                return -1;
            }
        } else {
            for (i = 1; i <= app.project.numItems; i++) items.push(app.project.item(i));
        }
        var picked = [], inames = [];
        for (i = 0; i < items.length; i++) {
            var it = items[i];
            var kind = it instanceof CompItem ? "comps" : it instanceof FolderItem ? "folders" : "footage";
            if (!o[kind]) continue;
            picked.push(it);
            inames.push(rep(it.name));
        }
        RK.undo(RK.NAME + " - Search and Replace", function () { n = RK.rt.renameItems(picked, inames, o.update); });
        return n;
    }
    // layers to work on
    var groups = [];   // [{comp, layers}]
    if (o.allComps) {
        var comps = RK.rt.allComps();
        for (i = 0; i < comps.length; i++) groups.push({ comp: comps[i], layers: RK.rt.layersOf(comps[i]) });
    } else {
        var comp = RK.comp();
        if (!comp) return -1;
        var ls = o.selectedLayers ? comp.selectedLayers : RK.rt.layersOf(comp);
        if (!ls.length) {
            RK.msg(o.selectedLayers ? "No layer is selected. Select a layer." : "There is no layer in this comp. Select a composition with layers.");
            return -1;
        }
        groups.push({ comp: comp, layers: ls });
    }
    if (o.mode === "layers") {
        var all = [], lnames = [];
        for (var g = 0; g < groups.length; g++) {
            for (var k = 0; k < groups[g].layers.length; k++) {
                all.push(groups[g].layers[k]);
                lnames.push(rep(groups[g].layers[k].name));
            }
        }
        RK.undo(RK.NAME + " - Search and replace", function () { n = RK.rt.renameLayers(all, lnames, o.update, o.project); });
        return n;
    }
    RK.undo(RK.NAME + " - Replace in Expressions", function () {
        for (var g = 0; g < groups.length; g++) n += RK.rt.editExpressions(groups[g].comp, groups[g].layers, rep);
    });
    return n;
};

RK.rt.replaceState = {
    mode: 0, allComps: false, selectedLayers: false, comps: true, footage: true, folders: true,
    selectedItems: false, search: "", caseSensitive: true, replace: "", update: true, scope: 0
};

RK.rt.replacePage = function (body) {
    var st = RK.rt.replaceState, X = RK.ui.expert;
    var mode = RK.ui.radios(body, ["Expressions", "Layers", X ? "Items" : "Project Items"], st.mode, { onChange: function () { sync(); } });
    var stack = body.add("group");
    stack.orientation = "stack";
    stack.alignChildren = ["fill", "top"];
    var gA = stack.add("group");
    gA.orientation = "column";
    gA.alignChildren = ["left", "top"];
    gA.spacing = 2;
    var where = RK.ui.radios(gA, [X ? "Active" : "Active comp", "All comps"], st.allComps ? 1 : 0, { onChange: function () { sync(); } });
    var which = RK.ui.radios(gA, [X ? "Selected" : "Selected layers", "All layers"], st.selectedLayers ? 0 : 1);
    var gB = stack.add("group");
    gB.orientation = "column";
    gB.alignChildren = ["left", "top"];
    gB.spacing = 2;
    var kinds = RK.ui.row(gB);
    var cComps = RK.ui.check(kinds, "Comps", st.comps);
    var cFoot = RK.ui.check(kinds, "Footage", st.footage);
    var cFold = RK.ui.check(kinds, "Folders", st.folders);
    var items = RK.ui.radios(gB, ["Selected Items", "All Items"], st.selectedItems ? 0 : 1);
    RK.ui.separator(body);
    var rS = RK.ui.row(body, "Search", 60);
    var eS = RK.ui.text(rS, st.search, RK.PAGE_W - 64);
    var cCase = RK.ui.check(body, "Case sensitive", st.caseSensitive);
    var rR = RK.ui.row(body, "Replace", 60);
    var eR = RK.ui.text(rR, st.replace, RK.PAGE_W - 64);
    RK.ui.separator(body);
    var upd = RK.ui.check(body, "Update expressions", st.update, "Update the expressions that refer to the renamed layers or comps");
    var scope = RK.ui.radios(body, ["Current comp", "Project"], st.scope);
    var sync = function () {
        var m = mode.getValue();
        gA.visible = m !== 2;
        gB.visible = m === 2;
        which.setEnabled(where.getValue() === 0);
        upd.enabled = m === 1 || (m === 2 && cComps.value);
        scope.setEnabled(m === 1 && upd.value);
    };
    var lastMode = mode.getValue();
    var onMode = function () {
        // Layers mode starts again from "Current comp"
        if (mode.getValue() === 1 && lastMode !== 1) scope.setValue(0);
        lastMode = mode.getValue();
    };
    mode.radios[0].onClick = (function (f) { return function () { f.call(this); onMode(); sync(); }; })(mode.radios[0].onClick);
    mode.radios[1].onClick = (function (f) { return function () { f.call(this); onMode(); sync(); }; })(mode.radios[1].onClick);
    mode.radios[2].onClick = (function (f) { return function () { f.call(this); onMode(); sync(); }; })(mode.radios[2].onClick);
    upd.onClick = sync;
    cComps.onClick = sync;
    sync();
    RK.ui.bottomRow(body, [RK.ui.backItem(), {
        label: "Replace", icon: RK.I.ok, tip: "Replace",
        onClick: RK.ui.action("Replace", function () {
            var m = mode.getValue();
            st.mode = m; st.allComps = where.getValue() === 1; st.selectedLayers = which.getValue() === 0;
            st.comps = cComps.value; st.footage = cFoot.value; st.folders = cFold.value; st.selectedItems = items.getValue() === 0;
            st.search = eS.text; st.caseSensitive = cCase.value; st.replace = eR.text; st.update = upd.value; st.scope = scope.getValue();
            RK.rt.replace({
                mode: m === 1 ? "layers" : m === 2 ? "items" : "expressions",
                search: st.search, replace: st.replace, caseSensitive: st.caseSensitive,
                allComps: st.allComps, selectedLayers: st.selectedLayers,
                comps: st.comps, footage: st.footage, folders: st.folders, selectedItems: st.selectedItems,
                update: m !== 0 && st.update && (m !== 2 || st.comps), project: m === 1 && st.scope === 1
            });
        })
    }]);
};

RK.tool("replace", {
    label: "Replace", icon: RK.I.replace,
    tip: "Searches and replaces text in expressions, layer names or project item names.",
    page: function (body) { RK.rt.replacePage(body); }
});

// ============================================================================ Rigging tab: Auto-Rig
//
// Builds whole limbs from layers the user assigns in floating windows: legs (three animal
// types), spine with neck and head, tail, or a full character (all of them in a row). It reuses
// the IK, IK Goal, Bezier IK, controller and zero builders.

RK.ar = { type: 2, full: null, wins: {} };
RK.ar.TYPES = ["Ungulate", "Digitigrade", "Plantigrade"];

RK.ar.COL = {
    orange: RK.rgb(224, 146, 62), lightOrange: RK.rgb(224, 188, 62), yellow: RK.rgb(222, 224, 62),
    blue: RK.rgb(62, 131, 224), purple: RK.rgb(150, 62, 224), red: RK.rgb(255, 68, 68),
    left: RK.rgb(0, 255, 0), right: RK.rgb(255, 0, 0)
};

// The slots of each window. key: where the layer goes; words: name detection keywords, tried
// in order; types: the animal types it shows for (default all); range: a From/To pair.
RK.ar.LIMBS = {
    back: {
        title: "Back Leg Autorig", undo: "Back Leg Autorig",
        slots: [
            { key: "H", label: "Femur, thigh", color: "lightOrange", words: ["femur", "thigh"] },
            { key: "R", label: "Tibia, fibula, calf, knee", color: "yellow", words: ["tibia", "fibula", "calf", "knee"] },
            { key: "K", label: "Tarsus, foot", color: "blue", words: ["tarsus", "foot"] },
            { key: "W", label: "Claws, hoof, toes", color: "purple", words: ["claws", "hoof", "toes"] },
            { header: "Null objects:", types: [1, 2] },
            { key: "T", label: "Tip, tiptoe", color: "red", words: ["tip", "tiptoe"], types: [1, 2] },
            { key: "E", label: "Heel, back, contact, palm", color: "red", words: ["heel", "back", "contact", "palm"], types: [2] }
        ]
    },
    front: {
        title: "Front Leg Autorig", undo: "Front Leg Autorig",
        slots: [
            { key: "S", label: "Shoulder blade, clavicle", color: "orange", words: ["shoulder blade", "blade", "clavicle", "shoulder"] },
            { key: "H", label: "Arm, humerus, shoulder", color: "lightOrange", words: ["humerus", "arm", "shoulder"] },
            { key: "R", label: "Ulna, radius, forearm, elbow", color: "yellow", words: ["ulna", "radius", "forearm", "elbow"] },
            { key: "K", label: "Carpus, palm, hand", color: "blue", words: ["carpus", "palm", "hand"] },
            { key: "W", label: "Claws, hoof, fingers", color: "purple", words: ["claws", "hoof", "finger"] },
            { header: "Null objects:", types: [1, 2] },
            { key: "T", label: "Tip, tiptoe", color: "red", words: ["tip", "tiptoe"], types: [1, 2] },
            { key: "E", label: "Heel, back, contact, palm", color: "red", words: ["heel", "back", "contact", "palm"], types: [2] }
        ]
    },
    spine: {
        title: "Spine Autorig", undo: "Spine Autorig",
        slots: [
            { key: "head", label: "Head", color: "orange", words: ["head"] },
            { header: "Neck", color: "yellow" },
            { key: "neckFrom", label: "From", color: "yellow", range: "neck" },
            { key: "neckTo", label: "To", color: "yellow", range: "neck" },
            { header: "Spine, torso, chest, thorax", color: "blue" },
            { key: "spineFrom", label: "From", color: "blue", range: "spine" },
            { key: "spineTo", label: "To", color: "blue", range: "spine" },
            { key: "hips", label: "Hips, pelvis, abdomen", color: "purple", words: ["hips", "pelvis", "abdomen"] }
        ]
    },
    tail: {
        title: "Tail Autorig", undo: "Tail Autorig",
        slots: [
            { key: "hips", label: "Hips, pelvis, abdomen", color: "purple", words: ["hips", "pelvis", "abdomen"] },
            { header: "Tail", color: "yellow" },
            { key: "tailFrom", label: "From", color: "yellow", range: "tail" },
            { key: "tailTo", label: "To", color: "yellow", range: "tail" }
        ]
    }
};

RK.ar.shown = function (slot, type) {
    if (!slot.types) return true;
    for (var i = 0; i < slot.types.length; i++) if (slot.types[i] === type) return true;
    return false;
};

// ---------------------------------------------------------------------------- name detection

// The layers to choose from: the selected ones (selection order), or all of them (stack order).
RK.ar.candidates = function (comp) {
    var sel = comp.selectedLayers;
    if (sel.length) return sel;
    var out = [];
    for (var i = 1; i <= comp.numLayers; i++) out.push(comp.layer(i));
    return out;
};

// The first candidate whose name contains one of the words (tried in order), with an optional
// side prefix ("L_" / "R_", then "L " / "R ").
RK.ar.detect = function (cands, words, side) {
    var prefixes = side ? [side + "_", side + " "] : [""];
    for (var p = 0; p < prefixes.length; p++) {
        for (var w = 0; w < words.length; w++) {
            var k = (prefixes[p] + words[w]).toLowerCase();
            for (var i = 0; i < cands.length; i++) if (String(cands[i].name).toLowerCase().indexOf(k) >= 0) return cands[i];
        }
    }
    return null;
};

RK.ar.detectAll = function (cands, words) {
    var out = [];
    for (var i = 0; i < cands.length; i++) {
        var n = String(cands[i].name).toLowerCase();
        for (var w = 0; w < words.length; w++) if (n.indexOf(words[w]) >= 0) { out.push(cands[i]); break; }
    }
    return out;
};

// Fill a limb's slots from the layer names. side: "L", "R" or null.
RK.ar.guess = function (kind, cands, side) {
    var a = {}, i;
    var slots = RK.ar.LIMBS[kind].slots;
    for (i = 0; i < slots.length; i++) if (slots[i].words) a[slots[i].key] = RK.ar.detect(cands, slots[i].words, side);
    var byDist = function (list, from, far) {
        if (!from) return list;
        var p = RK.world(from).pos;
        return list.slice().sort(function (x, y) {
            var dx = RK.dist(RK.world(x).pos, p), dy = RK.dist(RK.world(y).pos, p);
            return far ? dy - dx : dx - dy;
        });
    };
    if (kind === "spine") {
        var found = RK.ar.detectAll(cands, ["spine", "torso", "chest", "thorax"]);
        var neck = RK.ar.detectAll(cands, ["neck"]);
        var spine = byDist(found, a.hips, true);
        neck = byDist(neck, a.hips || found[0] || null, true);
        a.spineFrom = spine[0] || null;
        a.spineTo = spine[spine.length - 1] || null;
        a.neckFrom = neck[0] || null;
        a.neckTo = neck[neck.length - 1] || null;
    } else if (kind === "tail") {
        var tail = byDist(RK.ar.detectAll(cands, ["tail"]), a.hips, false);
        a.tailFrom = tail[0] || null;
        a.tailTo = tail[tail.length - 1] || null;
    }
    return a;
};

// The layers between two (inclusive), in stack order from `from` to `to`.
RK.ar.range = function (comp, from, to) {
    if (!from && !to) return [];
    if (!from || !to) return [from || to];
    var a = from.index, b = to.index, out = [], step = a <= b ? 1 : -1;
    for (var i = a; ; i += step) {
        out.push(comp.layer(i));
        if (i === b) break;
    }
    return out;
};

// ---------------------------------------------------------------------------- validation

// The layers a limb uses, for the duplicate and 3D checks.
RK.ar.used = function (kind, a, type, comp) {
    var out = [], k;
    if (kind === "front" || kind === "back") {
        var keys = kind === "front" ? ["S", "H", "R", "K", "W"] : ["H", "R", "K", "W"];
        if (type !== 0) keys.push("T");
        if (type === 2) keys.push("E");
        for (k = 0; k < keys.length; k++) if (a[keys[k]]) out.push(a[keys[k]]);
    } else if (kind === "spine") {
        if (a.head) out.push(a.head);
        if (a.hips) out.push(a.hips);
        out = out.concat(RK.ar.range(comp, a.neckFrom, a.neckTo), RK.ar.range(comp, a.spineFrom, a.spineTo));
    } else {
        if (a.hips) out.push(a.hips);
        out = out.concat(RK.ar.range(comp, a.tailFrom, a.tailTo));
    }
    return out;
};

// null when fine, else the message.
RK.ar.validate = function (kind, a, type, comp) {
    var used = RK.ar.used(kind, a, type, comp);
    if (!used.length) return null;
    var seen = {}, dup = [], i;
    for (i = 0; i < used.length; i++) {
        var id = used[i].id;
        if (seen[id] && dup.indexOf(used[i].index + " - " + used[i].name) < 0) dup.push(used[i].index + " - " + used[i].name);
        seen[id] = true;
    }
    if (dup.length) return "These layers are used twice:\n" + dup.join("\n");
    var miss = [];
    if (kind === "front" || kind === "back") {
        var hand = kind === "front" ? "Carpus / hand" : "Tarsus / foot";
        if (!a.K) miss.push(hand);
        if (a.W) {
            if (!a.H) miss.push(kind === "front" ? "Humerus / arm" : "Femur / thigh");
            if (!a.R) miss.push(kind === "front" ? "Radius / forearm" : "Tibia / calf");
        }
    } else if (kind === "spine") {
        if (!a.head) miss.push("Head");
        if (!a.hips && !a.spineFrom && !a.spineTo) miss.push("Spine and/or Hips");
    } else {
        if (!a.hips) miss.push("Hips");
        if (!a.tailFrom && !a.tailTo) miss.push("Tail(s)");
    }
    if (miss.length) return "Missing layers:\n" + miss.join("\n");
    for (i = 0; i < used.length; i++) if (RK.is3D(used[i])) return "The auto-rig doesn't work with 3D layers (" + used[i].name + ").";
    return null;
};

// ---------------------------------------------------------------------------- shared builders

// A null at a world position, named, just above a layer in the stack.
RK.ar.nullAt = function (comp, name, pos, above) {
    var n = comp.layers.addNull();
    n.name = RK.uniqueName(comp, name);
    n.transform.anchorPoint.setValue([0, 0, 0]);
    n.transform.position.setValue([pos[0], pos[1], 0]);
    if (above) RK.placeAbove(n, above);
    return n;
};

RK.ar.unparent = function (layer) {
    if (layer && layer.parent) RK.withUnlocked(layer, function () { layer.parent = null; });
};
RK.ar.parent = function (child, parent) {
    if (!child || !parent) return;
    if (child.parent && child.parent.id === parent.id) return;
    RK.withUnlocked(child, function () { child.parent = parent; });
};

// Build an IK the way the IK tool does (analysis of the given layers, in selection order),
// without lock-and-shy. fxLayer: the layer holding the IK parameters, when it isn't the target.
RK.ar.ik = function (comp, layers, fxLayer) {
    var a = RK.ik.analyse(comp, layers);
    if (!a) throw new Error("couldn't set up the IK on " + layers.map(function (l) { return l.name; }).join(", "));
    return RK.ik.build(comp, a, a.kind, { noLockShy: true, fxLayer: fxLayer || null });
};

// A one-bone IK whose effect is set to a weight, with FK compensating the jump.
// The layer looks at the controller at a share (weight %); the static value takes the
// difference when the expressions are written, so nothing jumps.
RK.ar.halfLook = function (comp, bone, ctrl, weight) {
    return RK.ik.one(comp, bone, ctrl, { weight: weight });
};

// A foot-roll formula on a pivot's rotation (mode: see the "roll" formula in 46_rkx.js).
RK.ar.roll = function (comp, layer, ctrl, group, mode) {
    RK.rkx.with(comp, function (b) {
        b.set(layer, "r", { k: "roll", m: mode, t: RK.rkx.fx(ctrl, RK.fxg(group, "Tiptoe")), h: RK.rkx.fx(ctrl, RK.fxg(group, "Heel")),
            r: RK.rkx.fx(ctrl, RK.fxg(group, "Foot roll")) }, "foot roll");
    });
};

// The shoulder follows a share of the controller's movement relative to the body (slider
// "<S> auto-position %" on the controller). The controller is seen from the shoulder's parent
// (its zero), so the share follows the body when it turns; the offset is measured from where
// the controller was at rig time, so nothing jumps.
RK.ar.autoPosition = function (comp, S, ctrl, pct) {
    var name = RK.uniqueEffectName(ctrl, S.name + " auto-position %");
    RK.fxOff(ctrl, RK.addSlider(ctrl, name, pct));
    var c0 = RK.world(ctrl).pos;
    var Z = S.parent;
    var l0 = Z ? RK.ik.toLayer(Z, c0) : c0;
    RK.rkx.with(comp, function (b) {
        b.set(S, "p", { k: "apos", c: ctrl, w: RK.rkx.fx(ctrl, name), l: [l0[0], l0[1]] }, "auto-position");
    });
};

// ---------------------------------------------------------------------------- legs

// a: {S, H, R, K, W, T, E} (layers or null). front: bool. type: 0 ungulate, 1 digitigrade,
// 2 plantigrade. Returns the main controller (or null when nothing was built).
RK.ar.leg = function (comp, a, front, type) {
    return RK.rkx.with(comp, function () { return RK.ar.leg_(comp, a, front, type); });
};
RK.ar.leg_ = function (comp, a, front, type) {
    var S = front ? a.S : null, H = a.H, R = a.R, K = a.K, W = a.W, T = type ? a.T : null, E = type === 2 ? a.E : null;
    if (!K || (W && (!H || !R))) return null;
    RK.nameCheck(comp);
    if (type === 0) return RK.ar.ungulate(comp, S, H, R, K, W);
    // 1. un-parent
    if (H && R) RK.ar.unparent(R);
    if (H || R) RK.ar.unparent(K);
    RK.ar.unparent(W);
    RK.ar.unparent(T);
    RK.ar.unparent(E);
    var kw = RK.world(K).pos, ww = W ? RK.world(W).pos : null;
    var right = W ? ww[0] > kw[0] : true;
    // 3. pivots
    var NW = null, NK = null;
    if (W) {
        NW = RK.ar.nullAt(comp, "IK " + W.name, ww, W);
        var size = ww[0] - kw[0];
        if (!T) T = RK.ar.nullAt(comp, "IK tiptoe " + W.name, [ww[0] + size, ww[1]], W);
        if (type === 2 && !E) E = RK.ar.nullAt(comp, "IK heel " + K.name, [kw[0], ww[1]], W);
        NK = RK.ar.nullAt(comp, "IK  " + K.name, kw, K);
    }
    // 4. controller and its zero
    var CK = RK.ctrl.create(comp, K, { flags: { rotation: true, x: true, y: true, scale: false }, select: false });
    RK.zero(CK);
    // 5. parenting
    if (W) RK.ar.parent(W, K);
    if (R) { RK.ar.parent(K, R); if (H) RK.ar.parent(R, H); } else if (H) RK.ar.parent(K, H);
    if (W) {
        RK.ar.parent(NK, NW);
        RK.ar.parent(NW, T);
        if (type === 2) { RK.ar.parent(T, E); RK.ar.parent(E, CK); } else RK.ar.parent(T, CK);
    }
    if (S) RK.ar.parent(H || R || K, S);
    // 6. IKs: the leg reaches the ankle pivot (its parameters on the controller), the foot
    // looks at the toes pivot; the toes look at the tip (step 8, once the foot roll exists)
    if (W) {
        RK.ar.ik(comp, [K, R, H, NK], CK);
        RK.ik.one(comp, K, NW);
    } else if (H && R) RK.ar.ik(comp, [K, R, H, CK]);
    else if (R) RK.ar.ik(comp, [K, R, CK]);
    else if (H) RK.ar.ik(comp, [K, H, CK]);
    // 7. shoulder
    if (S) {
        RK.zero(S);
        RK.ar.halfLook(comp, S, CK, 50);
    }
    // 8. controls on the controller
    if (W) {
        var roll = RK.uniqueEffectName(CK, K.name + " Foot roll");
        RK.addGroup(CK, roll, [{ name: "Tiptoe", type: "angle", value: 0 }, { name: "Heel", type: "angle", value: 0 }, { name: "Foot roll", type: "angle", value: 0 }]);
        // digitigrade: the toes' FK follows Heel
        RK.ik.one(comp, W, T, type === 1 ? { extraFK: [RK.rkx.fx(CK, RK.fxg(roll, "Heel"))] } : {});
        RK.ar.roll(comp, T, CK, roll, "tip");
        if (type === 1) RK.ar.roll(comp, NW, CK, roll, "nwd");
        else {
            RK.ar.roll(comp, E, CK, roll, right ? "er" : "el");
            RK.ar.roll(comp, NW, CK, roll, right ? "nwr" : "nwl");
        }
    }
    // 9. shoulder auto-position
    if (S) RK.ar.autoPosition(comp, S, CK, type === 1 ? 10 : 0);
    // the limb's layers are drawn flat (46_rkx.js): each one costs its own expressions only
    if (H || R) RK.rkx.cur.flatten([S, H, R, K, W]);
    // 10. hide the pivots
    if (W) {
        var piv = [T, NW, NK];
        if (type === 2) piv.push(E);
        for (var i = 0; i < piv.length; i++) RK.hide(piv[i], { off: true, shy: true, locked: true });
    }
    RK.selectOnly(comp, [CK]);
    return CK;
};

RK.ar.ungulate = function (comp, S, H, R, K, W) {
    if (H && R) RK.ar.unparent(R);
    if (H || R) RK.ar.unparent(K);
    RK.ar.unparent(W);
    var C = RK.ctrl.create(comp, W || K, { flags: { rotation: true, x: true, y: true, scale: false }, select: false });
    RK.zero(C);
    if (W) RK.ar.parent(W, K);
    if (R) { RK.ar.parent(K, R); if (H) RK.ar.parent(R, H); } else if (H) RK.ar.parent(K, H);
    if (S) RK.ar.parent(H || R || K, S);
    if (W) {
        RK.ar.ik(comp, [W, K, R, C]);
        if (H) RK.ik.one(comp, H, C);
    } else if (H && R) RK.ar.ik(comp, [K, R, H, C]);
    else if (R) RK.ar.ik(comp, [K, R, C]);
    else if (H) RK.ar.ik(comp, [K, H, C]);
    if (S) {
        RK.zero(S);
        RK.ar.halfLook(comp, S, C, 50);
        RK.ar.autoPosition(comp, S, C, 10);
    }
    if (H || R) RK.rkx.cur.flatten([S, H, R, K, W]);
    RK.selectOnly(comp, [C]);
    return C;
};

// ---------------------------------------------------------------------------- spine

// a: {hips, head, spine: [head side .. hips side], neck: [head side .. body side]}
RK.ar.spine = function (comp, a) {
    return RK.rkx.with(comp, function () { return RK.ar.spine_(comp, a); });
};
RK.ar.spine_ = function (comp, a) {
    var hips = a.hips, head = a.head, spine = a.spine || [], neck = a.neck || [];
    if (!head || (!hips && !spine.length)) return null;
    RK.nameCheck(comp);
    var hipsParent = hips ? hips.parent : null;
    var all = [].concat(hips ? [hips] : [], spine, neck, [head]);
    for (var i = 0; i < all.length; i++) RK.ar.unparent(all[i]);
    var base = hips || spine[spine.length - 1];
    var st = RK.ctrl.settings();
    var size = RK.ctrl.sizePx(comp, st);
    var flags = { rotation: true, x: true, y: true, scale: false };
    var big = RK.ctrl.create(comp, base, { flags: flags, select: false });
    if (big instanceof ShapeLayer) {
        RK.withUnlocked(big, function () {
            RK.ctrl.draw(big, flags, size * 1.5, [st.color[0] * 0.5, st.color[1] * 0.5, st.color[2] * 0.5]);
        });
    }
    var hc = RK.ctrl.create(comp, big, { flags: flags, select: false });
    var sh = null, hd;
    if (neck.length) {
        sh = RK.ctrl.create(comp, neck[neck.length - 1], { flags: flags, select: false, name: RK.uniqueName(comp, "C_Shoulders") });
        hd = RK.ctrl.create(comp, head, { flags: { rotation: true, x: false, y: false, scale: false }, select: false });
    } else {
        hd = RK.ctrl.create(comp, head, { flags: flags, select: false });
    }
    // body parenting
    if (hips) RK.ar.parent(hips, hc);
    if (spine.length) RK.ar.parent(spine[spine.length - 1], hips || hc);
    if (neck.length) RK.ar.parent(neck[neck.length - 1], spine[0] || hips);
    for (i = 0; i < neck.length - 1; i++) RK.ar.parent(neck[i], neck[i + 1]);
    RK.ar.parent(head, neck[0] || spine[0] || hips);
    // controller parenting
    RK.withUnlocked(big, function () { big.parent = hipsParent || null; });
    RK.ar.parent(hc, big);
    if (sh) RK.ar.parent(sh, big);
    RK.ar.parent(hd, sh || big);
    RK.withUnlocked(hd, function () {
        RK.ctrl.unlock(hd);
        RK.ctrl.lock(hd, neck.length ? { rotation: true, x: false, y: false, scale: false } : { rotation: true, x: true, y: true, scale: false });
    });
    // spine IK
    var endC = sh || hd;
    if (!spine.length) RK.ik.one(comp, hips, endC);
    else if (spine.length === 1) RK.ik.one(comp, spine[0], endC);
    else {
        var last = spine[spine.length - 1];
        var nul = RK.ar.nullAt(comp, "IK " + last.name, RK.world(last).pos, last);
        RK.ar.parent(nul, hc);
        var bones = spine.slice();   // head side first, like a tip-to-root selection
        var bz = RK.bz.build(comp, bones, endC, nul, false);   // its curve controller: "<end>_curve"
        RK.ar.parent(bz.K1, big);
        RK.hide(nul, { off: true, shy: true, locked: true });
    }
    // neck: each layer takes 1/N of the shoulders controller's rotation and, with the goal on,
    // gives back 1/N of the torso's turn
    if (neck.length) {
        var N = neck.length, sw = RK.uniqueEffectName(sh, neck[neck.length - 1].name + " goal");
        RK.fxOff(sh, RK.addCheckbox(sh, sw, true));
        var torso = spine[0] || hips;
        var T0 = RK.rotProp(torso).value + (spine.length <= 1 ? RK.rotProp(hc).value : 0);
        RK.rkx.with(comp, function (b) {
            for (var j = 0; j < N; j++) {
                b.set(neck[j], "r", { k: "neck", s: sh, o: RK.rkx.fx(sh, sw), t: torso, h: spine.length <= 1 ? hc : null, t0: T0, n: N }, "neck");
            }
        });
    }
    // head
    RK.ik.goal(comp, head, hd);
    RK.selectOnly(comp, [hd]);
    return { big: big, hips: hc, shoulders: sh, head: hd };
};

// ---------------------------------------------------------------------------- tail

// a: {hips, tail: [base .. tip]}, cubic: two curves
RK.ar.tail = function (comp, a, cubic) {
    return RK.rkx.with(comp, function () { return RK.ar.tail_(comp, a, cubic); });
};
RK.ar.tail_ = function (comp, a, cubic) {
    var hips = a.hips, tail = a.tail || [];
    if (!hips || !tail.length) return null;
    RK.nameCheck(comp);
    for (var i = 0; i < tail.length; i++) RK.ar.unparent(tail[i]);
    var base = tail[0], tip = tail[tail.length - 1];
    var tp = RK.world(tip).pos, pv = RK.world(tail.length > 1 ? tail[tail.length - 2] : hips).pos;
    var ctrl = RK.ctrl.create(comp, base, { flags: { rotation: false, x: true, y: true, scale: false }, select: false,
        position: [tp[0] + (tp[0] - pv[0]), tp[1] + (tp[1] - pv[1])] });
    var nul = RK.ar.nullAt(comp, "IK " + base.name, RK.world(base).pos, base);
    RK.ar.parent(nul, hips);
    var bones = tail.slice().reverse();   // tip first
    // curve controllers "C_<base>_curve" (and "IK <base>_curve" for two curves)
    var bz = RK.bz.build(comp, bones, ctrl, nul, cubic);
    RK.hide(nul, { off: true, shy: true, locked: true });
    RK.selectOnly(comp, [bz.K2 || bz.K1]);
    return { ctrl: ctrl, curves: bz.K2 ? [bz.K1, bz.K2] : [bz.K1] };
};

// ---------------------------------------------------------------------------- building from windows

RK.ar.buildOne = function (comp, kind, a, opts) {
    return RK.rkx.with(comp, function () { return RK.ar.buildOne_(comp, kind, a, opts); });
};
RK.ar.buildOne_ = function (comp, kind, a, opts) {
    opts = opts || {};
    var type = RK.ar.type;
    if (kind === "front" || kind === "back") return RK.ar.leg(comp, a, kind === "front", type);
    if (kind === "spine") {
        return RK.ar.spine(comp, { hips: a.hips, head: a.head, spine: RK.ar.range(comp, a.spineFrom, a.spineTo), neck: RK.ar.range(comp, a.neckFrom, a.neckTo) });
    }
    return RK.ar.tail(comp, { hips: a.hips, tail: RK.ar.range(comp, a.tailFrom, a.tailTo) }, !!opts.cubic);
};

// The whole character: legs, arms, spine, tail. f: {backL, backR, frontL, frontR, spine, tail, cubic}
RK.ar.buildFull = function (comp, f) {
    return RK.rkx.with(comp, function () { return RK.ar.buildFull_(comp, f); });
};
RK.ar.buildFull_ = function (comp, f) {
    var type = RK.ar.type;
    var spineList = f.spine ? RK.ar.range(comp, f.spine.spineFrom, f.spine.spineTo) : [];
    var hips = (f.spine && f.spine.hips) || (f.tail && f.tail.hips) || null;
    var attachLeg = function (a) {
        if (!a) return;
        var top = a.H || a.R || a.K;
        if (!top) return;
        if (hips) RK.ar.parent(top, hips);
        else if (spineList.length) RK.ar.parent(top, spineList[spineList.length - 1]);
    };
    var attachArm = function (a) {
        if (!a) return;
        var top = a.S || a.H || a.R || a.K;
        if (!top) return;
        if (spineList.length) RK.ar.parent(top, spineList[0]);
        else if (hips) RK.ar.parent(top, hips);
    };
    attachLeg(f.backL);
    attachLeg(f.backR);
    if (f.backL) RK.ar.leg(comp, f.backL, false, type);
    if (f.backR) RK.ar.leg(comp, f.backR, false, type);
    attachArm(f.frontL);
    attachArm(f.frontR);
    if (f.frontL) RK.ar.leg(comp, f.frontL, true, type);
    if (f.frontR) RK.ar.leg(comp, f.frontR, true, type);
    var sp = f.spine ? RK.ar.buildOne(comp, "spine", f.spine) : null;
    var tl = f.tail ? RK.ar.buildOne(comp, "tail", f.tail, { cubic: f.cubic }) : null;
    if (sp && tl) {
        RK.ar.parent(tl.ctrl, sp.big);
        for (var i = 0; i < tl.curves.length; i++) RK.ar.parent(tl.curves[i], sp.big);
    }
    return { spine: sp, tail: tl };
};

// ---------------------------------------------------------------------------- the windows

RK.ar.drawLimb = function (kind, type) {
    // schematic drawings, 100 wide; colours follow the rows
    var C = RK.ar.COL, out = [];
    var seg = function (col, pts) { out.push({ col: col, ops: [["L"].concat(pts), ["D", pts[0], pts[1], 3], ["D", pts[pts.length - 2], pts[pts.length - 1], 3]] }); };
    if (kind === "spine") {
        seg(C.orange, [36, 10, 36, 34]);
        out.push({ col: C.orange, ops: [["C", 36, 18, 12]] });
        seg(C.yellow, [36, 34, 36, 62]);
        seg(C.blue, [36, 62, 30, 100, 36, 140]);
        seg(C.purple, [36, 140, 36, 168]);
        return out;
    }
    if (kind === "tail") {
        seg(C.purple, [14, 30, 30, 40]);
        seg(C.yellow, [30, 40, 46, 62, 52, 90, 48, 120]);
        return out;
    }
    var front = kind === "front";
    if (front) seg(C.orange, [40, 10, 52, 40]);
    seg(C.lightOrange, [52, 40, 46, 92]);
    if (type === 0) {
        seg(C.yellow, [46, 92, 52, 140]);
        seg(C.blue, [52, 140, 50, 178]);
        seg(C.purple, [50, 178, 54, 192]);
    } else if (type === 1) {
        seg(C.yellow, [46, 92, 60, 138]);
        seg(C.blue, [60, 138, 54, 180]);
        seg(C.purple, [54, 180, 74, 186]);
        out.push({ col: C.red, ops: [["C", 82, 186, 3]] });
    } else {
        seg(C.yellow, [46, 92, 50, 150]);
        seg(C.blue, [50, 150, 56, 180]);
        seg(C.purple, [56, 180, 80, 184]);
        out.push({ col: C.red, ops: [["C", 88, 186, 3], ["C", 44, 188, 3]] });
    }
    return out;
};

RK.ar.closeWins = function () {
    for (var k in RK.ar.wins) {
        try {
            if (RK.ar.wins[k]) RK.ar.wins[k].close();
        } catch (e) {}
        RK.ar.wins[k] = null;
    }
};

// Open the window of one limb. step (full character): {side: "L"|"R", prev: fn, next: fn(a)}
RK.ar.open = function (kind, step) {
    var comp = RK.comp();
    if (!comp) return;
    RK.ar.closeWins();
    var type = RK.ar.type, def = RK.ar.LIMBS[kind];
    var cands = RK.ar.candidates(comp);
    var a = RK.ar.guess(kind, cands, step ? step.side : null);
    var items = ["None"], i;
    for (i = 0; i < cands.length; i++) items.push(cands[i].index + " - " + cands[i].name);
    var w = new Window("palette", def.title, undefined, { resizeable: true });
    RK.ar.wins[kind] = w;
    w.orientation = "column";
    w.alignChildren = ["fill", "top"];
    w.margins = [8, 8, 8, 8];
    w.spacing = 6;
    var top = w.add("group");
    top.orientation = "row";
    top.alignChildren = ["left", "top"];
    top.spacing = 10;
    var pic = top.add("statictext", undefined, "");
    var ph = kind === "spine" ? 178 : kind === "tail" ? 135 : 200;
    pic.preferredSize = [kind === "spine" || kind === "tail" ? 72 : 100, ph];
    pic.onDraw = function () {
        var parts = RK.ar.drawLimb(kind, type);
        for (var p = 0; p < parts.length; p++) RK.drawIcon(this.graphics, parts[p].ops, 0, 0, parts[p].col);
    };
    var col = top.add("group");
    col.orientation = "column";
    col.alignChildren = ["fill", "top"];
    col.spacing = 3;
    var colored = function (parent, text, c, w0) {
        var t = parent.add("statictext", undefined, text);
        t.preferredSize = [w0 || Math.ceil(RK.textWidth(text, RK.FONT)) + 4, 18];
        t.onDraw = function () { this.graphics.drawString(text, c ? this.graphics.newPen(this.graphics.PenType.SOLID_COLOR, c, 1) : null, 0, 1); };
        return t;
    };
    if (step && step.side) colored(col, (step.side === "L" ? "Left " : "Right ") + (kind === "front" ? "arm / front leg" : kind === "back" ? "leg" : kind), step.side === "L" ? RK.ar.COL.left : RK.ar.COL.right);
    colored(col, "Layers:", null);
    var drops = {};
    var dropW = 0;
    for (i = 0; i < items.length; i++) dropW = Math.max(dropW, RK.textWidth(items[i], RK.FONT));
    dropW = Math.min(260, Math.ceil(dropW) + 42);
    for (i = 0; i < def.slots.length; i++) {
        var sl = def.slots[i];
        if (!RK.ar.shown(sl, type)) continue;
        if (sl.header) {
            colored(col, sl.header, sl.color ? RK.ar.COL[sl.color] : null);
            continue;
        }
        var row = col.add("group");
        row.orientation = "stack";
        row.alignChildren = ["left", "center"];
        colored(row, sl.label, RK.ar.COL[sl.color], 190).alignment = ["left", "center"];
        var d = row.add("dropdownlist", undefined, items);
        d.preferredSize = [dropW, 22];
        d.alignment = ["right", "center"];
        row.preferredSize = [190 + 8 + dropW, 24];
        var cur = a[sl.key];
        var sel = 0;
        if (cur) for (var j = 0; j < cands.length; j++) if (cands[j].id === cur.id) sel = j + 1;
        d.selection = sel;
        drops[sl.key] = d;
    }
    var opts = null;
    if (kind === "tail") {
        colored(col, "Options:", null);
        opts = RK.ui.radios(col, ["Simple", "Two curves"], 0);
    }
    RK.ui.flat(col, { label: "Remove All", icon: RK.I.minus, h: 22, tip: "Set every slot to None",
        onClick: function () { for (var k in drops) drops[k].selection = 0; } }).alignment = ["left", "top"];
    var read = function () {
        var out = {};
        for (var k in drops) {
            var s = drops[k].selection ? drops[k].selection.index : 0;
            out[k] = s > 0 ? cands[s - 1] : null;
        }
        return out;
    };
    var bottom = w.add("group");
    bottom.orientation = "row";
    bottom.alignment = ["center", "top"];
    bottom.margins = [10, 6, 10, 0];
    bottom.spacing = 10;
    var check = function () {
        var c = RK.comp();
        if (!c) return null;
        var v = read();
        var err = RK.ar.validate(kind, v, RK.ar.type, c);
        if (err) {
            RK.msg(err);
            alertBox(err);
            return null;
        }
        return v;
    };
    var status = w.add("statictext", undefined, "");
    status.visible = false;
    var alertBox = function (t) {
        status.text = RK.wrap(t, 60);
        status.visible = true;
        try { w.layout.layout(true); } catch (e) {}
    };
    RK.ui.flat(bottom, { label: "Cancel", icon: RK.I.back, h: 24, center: true, onClick: function () { RK.ar.full = null; w.close(); RK.ar.wins[kind] = null; } });
    if (step && step.prev) RK.ui.flat(bottom, { label: "Previous", icon: RK.I.prev, h: 24, center: true, onClick: function () { w.close(); step.prev(); } });
    if (step && step.next) {
        RK.ui.flat(bottom, { label: "Next", icon: RK.I.next, h: 24, center: true, onClick: function () {
            var v = check();
            if (!v) return;
            if (opts) v.cubic = opts.getValue() === 1;
            w.close();
            step.next(v);
        } });
    }
    if (!step || step.ok) {
        RK.ui.flat(bottom, { label: "OK", icon: RK.I.ok, h: 24, center: true, onClick: function () {
            var v = check();
            if (!v) return;
            if (opts) v.cubic = opts.getValue() === 1;
            if (step && step.ok) {
                w.close();
                step.ok(v);
                return;
            }
            var c = RK.comp();
            try {
                RK.undo(RK.undoName(def.undo), function () { RK.ar.buildOne(c, kind, v, { cubic: v.cubic }); });
                w.close();
                RK.ar.wins[kind] = null;
            } catch (e) {
                alertBox(def.title + ": " + RK.err(e));
            }
        } });
    }
    w.show();
    return w;
};

// Full character: back legs (left, right), front legs (left, right), spine, tail.
RK.ar.fullCharacter = function () {
    var steps = [["back", "L"], ["back", "R"], ["front", "L"], ["front", "R"], ["spine", null], ["tail", null]];
    var keys = ["backL", "backR", "frontL", "frontR", "spine", "tail"];
    RK.ar.full = {};
    var go = function (i) {
        var s = steps[i];
        RK.ar.open(s[0], {
            side: s[1],
            prev: i > 0 ? function () { go(i - 1); } : null,
            next: i < steps.length - 1 ? function (v) { RK.ar.full[keys[i]] = RK.ar.empty(v) ? null : v; go(i + 1); } : null,
            ok: i === steps.length - 1 ? function (v) {
                RK.ar.full[keys[i]] = RK.ar.empty(v) ? null : v;
                RK.ar.full.cubic = !!v.cubic;
                var c = RK.comp();
                if (!c) return;
                try {
                    RK.undo(RK.undoName("Autorig"), function () { RK.ar.buildFull(c, RK.ar.full); });
                } catch (e) {
                    RK.msg("Auto-Rig: " + RK.err(e));
                }
                RK.ar.full = null;
            } : null
        });
    };
    go(0);
};

RK.ar.empty = function (v) {
    for (var k in v) if (v[k] && k !== "cubic") return false;
    return true;
};

// ---------------------------------------------------------------------------- the page

RK.ar.I = {
    ungulate: [["E", 9, 9, 6, 6.5], ["L", 9, 3, 9, 15]],
    digitigrade: [["D", 9, 11, 4], ["D", 3, 5, 1.8], ["D", 7, 3, 1.8], ["D", 12, 3, 1.8], ["D", 16, 5, 1.8]],
    plantigrade: [["E", 9, 10, 4.5, 6], ["D", 4, 2.5, 1.2], ["D", 7.5, 1.5, 1.2], ["D", 11, 1.5, 1.2], ["D", 14.5, 2.5, 1.2]],
    full: RK.I.autorig,
    front: [["L", 4, 2, 8, 9, 14, 12, 20, 12], ["D", 8, 9, 1.3], ["D", 14, 12, 1.3], ["L", 20, 10, 23, 11], ["L", 20, 14, 23, 13]],
    back: [["L", 6, 1, 10, 8, 8, 13, 18, 14], ["D", 10, 8, 1.3], ["D", 8, 13, 1.3]],
    spine: [["C", 12, 3, 2.5], ["L", 12, 5.5, 11, 9, 12, 12, 11, 15], ["L", 9, 9, 13, 9], ["L", 9, 12, 13, 12]],
    tail: [RK.op("L", RK.bez(3, 4, 14, 2, 8, 14, 21, 13, 10)), ["D", 3, 4, 1.6]]
};

RK.tool("autorig", {
    label: "Auto-Rig", icon: RK.I.autorig,
    tip: "Auto-Rig: rig legs, arms, spine and tail from named layers, or a whole character.",
    page: function (body) {
        var types = [{ label: "Ungulate", icon: RK.ar.I.ungulate, tip: "Horses, cattle, giraffes, pigs, deer, camels, hippopotamuses..." },
            { label: "Digitigrade", icon: RK.ar.I.digitigrade, tip: "Dogs, cats, dinosaurs, walking birds..." },
            { label: "Plantigrade", icon: RK.ar.I.plantigrade, tip: "Bears, humans and other primates, rabbits..." }];
        var tr = RK.ui.iconRadios(body, types, { value: RK.ar.type, center: true, onChange: function (i) { RK.ar.type = i; } });
        tr.alignment = ["center", "top"];
        RK.ui.separator(body);
        var full = RK.ui.flat(body, { label: "Full character", icon: RK.ar.I.full, center: true, name: "ar.full",
            tip: "Rig the whole character: back legs, front legs, spine and tail, one window after the other",
            onClick: function () { RK.clearMsg(); try { RK.ar.fullCharacter(); } catch (e) { RK.msg("Auto-Rig: " + RK.err(e)); } } });
        full.alignment = ["center", "top"];
        var cols = body.add("group");
        cols.alignment = ["center", "top"];
        cols.alignChildren = ["left", "top"];
        cols.spacing = 6;
        var mk = function (parent, label, icon, kind, tip) {
            return RK.ui.flat(parent, { label: label, icon: icon, w: 150, name: "ar." + kind, tip: tip,
                onClick: function () { RK.clearMsg(); try { RK.ar.open(kind); } catch (e) { RK.msg("Auto-Rig: " + RK.err(e)); } } });
        };
        var c1 = cols.add("group");
        c1.orientation = "column";
        c1.spacing = 2;
        mk(c1, "Front leg / Arm", RK.ar.I.front, "front", "Rig an arm or a front leg");
        mk(c1, "Spine - Neck - Head", RK.ar.I.spine, "spine", "Rig the spine, neck and head");
        var c2 = cols.add("group");
        c2.orientation = "column";
        c2.spacing = 2;
        mk(c2, "Back leg", RK.ar.I.back, "back", "Rig a leg or a back leg");
        mk(c2, "Tail", RK.ar.I.tail, "tail", "Rig a tail");
        RK.ui.bottomRow(body, [RK.ui.backItem("Cancel")]);
    }
});

// ============================================================================ Automation tab
//
// Wiggle, Swing, Spring, Blink, Orient to path, Wheel, Lens, Move away, Distance link,
// Exposure, Paint rigging, Paint group, Randomize and Time remap. No tool creates layers: they
// add expression controls (a custom control effect is an effect group, see RK.addGroup),
// expressions and keyframes, or change layer timing.
//
// How the expressions are written: RK.xHead + RK.xFast + RK.auto.XF, then every value that
// lives on another property is read through rk() with the fast host reads, so all the values
// that don't depend on each other arrive in one run; a frame costs two or three runs of each
// expression. A layer's world position is its parent's world transform applied to its
// Position: unlike toWorld(anchorPoint), this needs neither the layer's own rotation nor its
// scale, which are often driven by expressions (sometimes the very one being evaluated).

RK.auto = { I: {} };

// ---------------------------------------------------------------------------- icons (24 x 16)

(function () {
    var I = RK.auto.I;
    // a sine wave from x0 to x1 around y, with `periods` periods, decaying by exp(-damp * u)
    var wave = function (x0, x1, y, amp, periods, damp, n) {
        var out = [];
        for (var i = 0; i <= n; i++) {
            var u = i / n;
            out.push(x0 + (x1 - x0) * u, y - amp * Math.exp(-damp * u) * Math.sin(2 * Math.PI * periods * u));
        }
        return out;
    };
    I.wiggle = [["L", 1.5, 9, 3.5, 5, 5.5, 11.5, 8, 2.5, 10.5, 13, 12.5, 6, 14.5, 10, 16.5, 3.5, 19, 11.5, 21, 6.5, 22.5, 8.5]];
    I.swing = [RK.op("L", wave(1.5, 22.5, 8, 5.5, 2, 0, 32))];
    I.spring = [
        ["L", 1.5, 2.5, 1.5, 13.5],
        ["L", 1.5, 8, 3.5, 8, 5, 3.5, 7.5, 12.5, 10, 3.5, 12.5, 12.5, 15, 3.5, 16.5, 8, 17.6, 8],
        ["D", 20.4, 8, 2.8]
    ];
    I.blink = [["D", 6.5, 8, 4.6], ["C", 17.5, 8, 4.6]];
    I.orient = [RK.op("L", RK.bez(2, 14, 5, 3, 13, 1, 18.8, 7.3, 14)), ["P", 22.3, 11, 17, 9.1, 20.6, 5.5]];
    I.wheel = [
        ["C", 8.5, 8, 6.3], ["L", 8.5, 1.7, 8.5, 14.3], ["L", 2.2, 8, 14.8, 8], ["D", 8.5, 8, 1.4],
        RK.op("L", RK.arc(8.5, 8, 10, -38, 38, 10)), ["L", 19.6, 12.9, 16.4, 14.2, 16.8, 10.7]
    ];
    I.lens = [
        ["D", 4.5, 11.5, 3], ["C", 11.5, 7.6, 2.2], ["C", 16.6, 4.8, 1.5], ["D", 20.9, 2.5, 1.2],
        ["L", 7.3, 9.9, 9.3, 8.8], ["L", 13.5, 6.5, 15.2, 5.6], ["L", 18, 4, 19.7, 3.1]
    ];
    I.moveaway = [["F", 1.5, 5.5, 5, 5], ["L", 8.5, 8, 16.5, 8], ["L", 13.5, 5, 16.5, 8, 13.5, 11], ["C", 20.3, 8, 2.6]];
    I.distlink = [["R", 1.5, 4.5, 5, 7], ["R", 17.5, 4.5, 5, 7], ["L", 8, 8, 9.5, 8], ["L", 11.25, 8, 12.75, 8], ["L", 14.5, 8, 16, 8]];
    I.exposure = [["L", 2, 14.5, 2, 11.3, 7, 11.3, 7, 7.9, 12, 7.9, 12, 4.6, 17, 4.6, 17, 1.5, 22, 1.5]];
    I.paintrig = [
        ["L", 21.5, 1.5, 14, 9],
        ["P", 13, 7.6, 15.4, 10, 13.6, 11.8, 11.2, 9.4],
        ["P", 11.2, 9.4, 13.6, 11.8, 9.5, 14.8, 7.6, 15.2, 8.2, 13],
        RK.op("L", RK.bez(1, 12.5, 2.5, 8.5, 4.5, 15.5, 6.5, 12.5, 8))
    ];
    I.paintgroup = [
        ["L", 10, 1.5, 5.8, 7.7], ["P", 4.8, 6.9, 6.8, 8.9, 3, 14.5, 1.6, 13.4],
        ["L", 21.5, 1.5, 17.3, 7.7], ["P", 16.3, 6.9, 18.3, 8.9, 14.5, 14.5, 13.1, 13.4],
        ["L", 1.5, 15.5, 22.5, 15.5]
    ];
    I.randomize = [
        ["D", 3, 4, 1.4], ["D", 7.5, 11.5, 1.4], ["D", 11, 4.5, 1.4], ["D", 14.5, 13, 1.4],
        ["D", 17.5, 3.5, 1.4], ["D", 21, 9.5, 1.4], ["D", 11.5, 9, 0.9], ["D", 4, 14, 0.9]
    ];
    I.timeremap = [
        ["L", 1, 8, 23, 8],
        ["P", 5, 5, 8, 8, 5, 11, 2, 8], ["Z", 12, 5, 15, 8, 12, 11, 9, 8], ["P", 19, 5, 22, 8, 19, 11, 16, 8]
    ];
    I.refresh = [RK.op("L", RK.arc(10, 8, 5.5, -50, 230, 14)), ["L", 14.5, 1.6, 13.6, 4.2, 16.4, 4.6]];
})();

// ---------------------------------------------------------------------------- session state

// What the tool pages show. Pages are rebuilt each time they open; their values last for the
// session (they are not saved in the settings).
RK.auto.st = {
    wiggleSep: false,
    springSim: true,
    wheelRadius: "",
    wheelCurved: false,
    exp: {
        mode: 1,                      // 0 Fixed, 1 Adaptative, 2 From Footage
        lower: "1", upper: "4", precision: "500", sync: true, syncAll: true,
        layerItems: [], layerSel: -1,
        accuracy: "50", tolerance: "10", accSlider: 50, tolSlider: 10,
        chan: [true, true, true, false],
        status: "Analyze layer to detect exposure"
    },
    rnd: {
        props: true, start: false, inP: false, outP: false, times: false, values: false,
        max: ["Max", "Max", "Max"], min: ["Min", "Min", "Min"], maxS: [0, 0, 0], minS: [0, 0, 0],
        fromCurrent: true
    },
    tr: { loop: false, loopIn: false }
};

// The last footage analysis (Exposure > From Footage): {name, times (comp seconds)}.
RK.auto.footage = null;

// ---------------------------------------------------------------------------- shared helpers

RK.auto.undo = function (tool, fn) {
    return RK.undo(RK.NAME + " - " + tool, fn);
};

// Properties a numeric expression or random value makes sense on: numbers, 2D and 3D values
// and colours that interpolate (not checkboxes, menus, paths or text).
RK.auto.numeric = function (p) {
    var t;
    try {
        t = p.propertyValueType;
    } catch (e) {
        return false;
    }
    var V = PropertyValueType;
    if (t !== V.OneD && t !== V.TwoD && t !== V.TwoD_SPATIAL && t !== V.ThreeD && t !== V.ThreeD_SPATIAL && t !== V.COLOR) return false;
    try {
        return p.isInterpolationTypeValid(KeyframeInterpolationType.LINEAR) !== false;
    } catch (e2) {
        return true;
    }
};

RK.auto.isColor = function (p) {
    return p.propertyValueType === PropertyValueType.COLOR;
};

// Number of independent values for the "separate dimensions" rule (spec 20 §3.1): 1, 2 or 3,
// a 2D layer's Position, Scale and Anchor Point count as 2D, colours as one.
RK.auto.dims = function (p) {
    var d = RK.dims(p);
    return d === 4 ? 1 : d;
};

// Value properties among a layer's selected properties, in selection order.
RK.auto.layerProps = function (layer) {
    var all = layer.selectedProperties, out = [], seen = {};
    for (var i = 0; i < all.length; i++) {
        var p = all[i];
        if (!(p instanceof Property) || seen[p.__uid]) continue;
        seen[p.__uid] = true;
        out.push(p);
    }
    return out;
};

// "The selected property" (spec 00 §4, S5) with its messages: {comp, layer, prop} or null.
RK.auto.current = function () {
    var comp = RK.comp();
    if (!comp) return null;
    var p = RK.selectedProperty(comp);
    if (!p) return null;
    return { comp: comp, layer: RK.layerOf(p), prop: p };
};

// Swing and Blink want exactly one selected layer and one of its properties.
RK.auto.oneLayerProp = function (what) {
    var comp = RK.comp();
    if (!comp) return null;
    var sel = comp.selectedLayers;
    if (sel.length !== 1) {
        RK.msg(sel.length ? "Select only one layer, and the property to " + what + "." : "No layer selected: select the layer and the property to " + what + ".");
        return null;
    }
    var props = sel[0].selectedProperties;
    if (!props.length) {
        RK.msg("No property selected: select the property to " + what + ".");
        return null;
    }
    var p = props[props.length - 1];
    if (!RK.canExpr(p)) {
        RK.msg("An expression can't be created on " + p.name + ", so it can't be rigged.");
        return null;
    }
    return { comp: comp, layer: sel[0], prop: p };
};

RK.auto.needNumeric = function (c, tool) {
    if (RK.auto.numeric(c.prop)) return true;
    RK.msg(tool + " works on numbers, positions, scales, angles and colours; " + c.prop.name + " isn't one.");
    return false;
};

// Difference between two values (numbers or arrays): absolute value or Euclidean length.
RK.auto.diff = function (a, b) {
    if (!(a instanceof Array)) return Math.abs(a - b);
    var s = 0;
    for (var i = 0; i < a.length; i++) {
        var d = a[i] - (b[i] || 0);
        s += d * d;
    }
    return Math.sqrt(s);
};

// Runs fn, then gives the comp its selection back (layers in their selection order, then the
// properties): adding and removing a temporary layer clears it.
RK.auto.keepSelection = function (comp, fn) {
    var layers = comp.selectedLayers, props = comp.selectedProperties;
    try {
        return fn();
    } finally {
        try {
            RK.selectOnly(comp, layers);
            for (var i = 0; i < props.length; i++) props[i].selected = true;
        } catch (e) {}
    }
};

// World position [x, y, z] of a layer's anchor point at time t: its parent's world transform
// applied to its Position (cameras and lights too, which have no anchor point). May probe with
// a temporary layer: call inside an undo group.
RK.auto.worldPos = function (layer, t) {
    var comp = layer.containingComp, tr = layer.transform, p;
    if (t === undefined) t = comp.time;
    if (tr.position.dimensionsSeparated) {
        var z = tr.property("ADBE Position_2");
        p = [tr.property("ADBE Position_0").valueAtTime(t, false), tr.property("ADBE Position_1").valueAtTime(t, false), z ? z.valueAtTime(t, false) : 0];
    } else {
        p = tr.position.valueAtTime(t, false);
        p = [p[0], p[1], p.length > 2 ? p[2] : 0];
    }
    var par = layer.parent;
    if (!par) return p;
    var r = RK.probe(par, "var p = L.toWorld(" + JSON.stringify(p) + ", T);\n[p[0], p[1], p.length > 2 ? p[2] : 0, 0]", t);
    return [r[0], r[1], r[2]];
};

// A number typed in a field, or NaN.
RK.auto.parse = function (text) {
    var s = RK.trim(String(text)).replace(",", ".");
    if (!/^[-+]?(\d+\.?\d*|\.\d+)([eE][-+]?\d+)?$/.test(s)) return NaN;
    return parseFloat(s);
};

// ---------------------------------------------------------------------------- expression text

// Fast reads at other times than now, world conversions at a time, key times, an effect's
// child count; each falls back to the object model like RK.xFast's helpers.
RK.auto.XF =
    "function rkAt(L, path, t, api) { return rkF && path ? __h(__VALUE, thisComp.__c, L, path, t, false) : api(L, t); }\n" +
    "function rkXfAt(L, t) { return rkF ? __h(__XFORM, thisComp.__c, L, t) : L; }\n" +
    "function rkWt(X, p, t) { return rkF ? __m4(X[2], p, false) : X.toWorld(p, t); }\n" +
    "function rkFv(X, v, t) { return rkF ? __m4(X[3], v, true) : X.fromWorldVec(v, t); }\n" +
    "function rkKeys(L, path, api) { return rkF && typeof __PINFO === \"number\" ? __h(__PINFO, thisComp.__c, L, path)[4] : api(L); }\n" +
    "function rkKids(L, key, api) { if (!rkF || typeof __CHILD !== \"number\") return api(L); var r = __h(__CHILD, thisComp.__c, L, \"effects\", key); if (r === null) throw new Error(\"Effect \\\"\" + key + \"\\\" not found\"); return r[4]; }\n" +
    "function rkNeed(v, what) { if (v === null || v === undefined) throw new Error(what + \" not found\"); return v; }\n";

// Adds a number to a value: to a number, to every component of a 2D or 3D value, to the red,
// green and blue of a colour (its alpha stays).
RK.auto.ADD =
    "function rkAdd(v, o) { if (typeof v === \"number\") return v + o; var r = []; for (var i = 0; i < v.length; i++) r.push(i < 3 ? v[i] + o : v[i]); return r; }\n";

// The two expressions that scan many frames (curved Wheel, simulated Spring) run their loops as
// functions compiled with new Function: outside the expression text, their arithmetic isn't
// rewritten into array-maths calls, which halves their cost. They are compiled once per thread
// and kept in a global (expression globals last between evaluations): change a body, change
// its name. They read through the reader functions the expression passes (fast host reads or
// the object model); a value still on its way makes them return null after asking for every
// value they could.
RK.auto.SCAN_PENDING = "var chk = function (e) { if (String(e && e.message).charCodeAt(0) !== 1) throw e; };" +
    " var miss = false;" +
    " var get = function (t) { var p = null, X = null, w = null, m = false;" +
    " try { p = rp(t); } catch (e1) { chk(e1); m = true; }" +
    " try { X = rx(t); } catch (e2) { chk(e2); m = true; }" +
    " if (!m) { try { w = rw(X, p, t); } catch (e3) { chk(e3); m = true; } }" +
    " if (m) { miss = true; return null; } return w; };";

// Curved wheel: the signed distance rolled from frame n0 to frame n1 (+ moving right).
RK.auto.ROLL_FN = "rkAutoRoll1";
RK.auto.ROLL = RK.auto.SCAN_PENDING +
    " var D = 0, q = null;" +
    " for (var i = n0; i <= n1; i++) {" +
    " var w = get(i * dt);" +
    " if (w !== null && q !== null) { var dx = w[0] - q[0], dy = w[1] - q[1], dz = (w.length > 2 ? w[2] : 0) - (q.length > 2 ? q[2] : 0);" +
    " var d = Math.sqrt(dx * dx + dy * dy + dz * dz); D += dx > 0 ? d : -d; }" +
    " q = w; }" +
    " return miss ? null : D;";

// Simulated spring: [rest frame, last moving frame, world velocity now (x, y), world velocity
// at the last moving frame (x, y)]. Frames are scanned back from frame n: the first with no
// motion is the latest rest; going on, the first moving one ends the scan. Missing frames are
// asked for 64 at a time, then all the rest at once.
RK.auto.SPRING_FN = "rkAutoSpring1";
RK.auto.SPRING = RK.auto.SCAN_PENDING +
    " var vel = function (t) { var a = get(t), b = get(t - 0.01); return a === null || b === null ? null : [(a[0] - b[0]) * 100, (a[1] - b[1]) * 100]; };" +
    " var vt = vel(now), tr = -1, ts = -1, ask = -1, known = 0;" +
    " for (var i = n; i >= 0; i--) {" +
    " var v = vel(i * dt);" +
    " if (ask >= 0 || v === null) { if (ask < 0) ask = known < 64 ? 64 : 1000000; if (ask-- <= 0) break; continue; }" +
    " known++;" +
    " var still = v[0] === 0 && v[1] === 0;" +
    " if (tr < 0) { if (still) tr = i; } else if (!still) { ts = i; break; } }" +
    " if (tr < 0) tr = 0;" +
    " if (ts < 0) ts = 0;" +
    " var vs = miss ? null : vel(ts * dt);" +
    " return miss ? null : [tr, ts, vt[0], vt[1], vs[0], vs[1]];";

// `name = <the compiled function>` once per thread.
RK.auto.xCompile = function (name, args, body) {
    return "if (typeof " + name + " !== \"function\") " + name + " = new Function(" +
        args.map(function (a) { return RK.q(a); }).join(", ") + ", " + RK.q(body) + ");\n";
};

RK.auto.head = function (tool) {
    return RK.xHead(tool) + RK.xFast + RK.auto.XF;
};

// This layer as a fast-path layer: its id is at hand (no lookup run), and so is its parent.
RK.auto.xMe = "var me = rkF && thisLayer.__l !== undefined ? thisLayer.__l : rk(function () { return rkLayer(thisLayer.index); });\n";

RK.auto.xLayer = function (v, name) {
    return "var " + v + " = rk(function () { return rkLayer(" + RK.q(name) + "); });\n";
};

// The first parameter of effect `name` on layer variable L.
RK.auto.xfx = function (v, L, name) {
    return "var " + v + " = " + L + " == null ? null : rk(function () { return rkNeed(rkVal(" + L + ", " + RK.q(RK.fxPath(name)) +
        ", function (L) { return L.effect(" + RK.q(name) + ")(1).value; }), " + RK.q("Effect \"" + name + "\"") + "); });\n";
};

// Reads for the world position of layer variable L's anchor point: <w>p its Position (or the
// code `own`), <w>a its parent (false without one), <w>x the parent's world transform.
RK.auto.xWorldReads = function (w, L, own) {
    return "var " + w + "p = " + (own || L + " == null ? null : rk(function () { return rkVal(" + L +
        ", \"transform/position\", function (L) { return L.transform.position.value; }); })") + ";\n" +
        "var " + w + "a = " + L + " == null ? null : rk(function () { return rkParent(" + L + "); });\n" +
        "var " + w + "x = " + w + "a == null ? null : (" + w + "a === false ? false : rk(function () { return rkXf(" + w + "a); }));\n";
};
// ...and the position, [x, y, z], once nothing is missing.
RK.auto.xWorld = function (w) {
    return "    var " + w + " = " + w + "x === false ? " + w + "p : rkW(" + w + "x, " + w + "p);\n" +
        "    " + w + " = [" + w + "[0], " + w + "[1], " + w + ".length > 2 ? " + w + "[2] : 0];\n";
};

// A radio button or check box placed directly in a page body sits on the left, like the texts.
RK.auto.left = function (c) {
    c.alignment = ["left", "center"];
    return c;
};

// The first of the layers whose Position has separated dimensions (an expression on the whole
// Position is then not used), or null.
RK.auto.separated = function (layers) {
    for (var i = 0; i < layers.length; i++) {
        try {
            if (layers[i].transform.position.dimensionsSeparated) return layers[i];
        } catch (e) {}
    }
    return null;
};

// Sets an expression on a property of a layer that may be locked.
RK.auto.expr = function (prop, code) {
    return RK.withUnlocked(RK.layerOf(prop), function () { return RK.setExpr(prop, code); });
};

// ---------------------------------------------------------------------------- Wiggle (§3.1)

RK.auto.wiggleParams = function (D, loop) {
    var out = [], k, ax = ["X", "Y", "Z"];
    if (D === 1) {
        out.push({ name: "Amplitude", type: "slider", value: 0 }, { name: "Frequency", type: "slider", value: 0 });
    } else {
        for (k = 0; k < D; k++) out.push({ name: ax[k] + " Amplitude", type: "slider", value: 0 });
        for (k = 0; k < D; k++) out.push({ name: ax[k] + " Frequency", type: "slider", value: 0 });
    }
    out.push({ name: "Loop Duration (s)", type: "slider", value: loop });
    return out;
};

// out = (1 - e(u/L)) W(u - L) + e(u/L) W(u - 2L) + V(t) - V(0), u = t mod L, W = wiggle at a
// time: the noise blends from the wiggle at -L to the one at -2L over each loop, so it repeats
// every L seconds without a seam.
RK.auto.wiggleExpr = function (g, D) {
    var s = RK.auto.head("wiggle") + RK.auto.xMe, k, ax = ["X", "Y", "Z"];
    if (D === 1) {
        s += RK.auto.xfx("A", "me", RK.fxg(g, "Amplitude")) + RK.auto.xfx("F", "me", RK.fxg(g, "Frequency"));
    } else {
        for (k = 0; k < D; k++) {
            s += RK.auto.xfx("A" + k, "me", RK.fxg(g, ax[k] + " Amplitude")) + RK.auto.xfx("F" + k, "me", RK.fxg(g, ax[k] + " Frequency"));
        }
    }
    s += RK.auto.xfx("Ld", "me", RK.fxg(g, "Loop Duration (s)")) +
        "var v0 = rk(function () { return valueAtTime(0); });\n" +
        // the times Loop Duration's usual value (the comp duration) samples, asked for now
        "var D0 = thisComp.duration, u0 = time % D0;\n" +
        "rk(function () { return valueAtTime(u0 - D0); });\n" +
        "rk(function () { return valueAtTime(u0 - 2 * D0); });\n" +
        "var e = 0, w1 = null, w2 = null;\n" +
        "if (!rk_wait) {\n" +
        "    var L = Ld > 0 ? Ld : thisComp.duration;\n" +
        "    var u = time % L;\n" +
        "    e = ease(u, 0, L, 0, 1);\n";
    if (D === 1) {
        s += "    w1 = rk(function () { return wiggle(Math.max(F, 0), A, 1, 0.5, u - L); });\n" +
            "    w2 = rk(function () { return wiggle(Math.max(F, 0), A, 1, 0.5, u - 2 * L); });\n" +
            "}\n" +
            "if (rk_wait) { value; } else { (1 - e) * w1 + e * w2 + value - v0; }";
        return s;
    }
    var a = [], f = [];
    for (k = 0; k < D; k++) {
        a.push("A" + k);
        f.push("F" + k);
    }
    s += "    var A = [" + a.join(", ") + "], F = [" + f.join(", ") + "];\n" +
        "    w1 = [];\n" +
        "    w2 = [];\n" +
        "    for (var k = 0; k < " + D + "; k++) {\n" +
        "        w1.push(rk(function () { return wiggle(Math.max(F[k], 0), A[k], 1, 0.5, u - L)[k]; }));\n" +
        "        w2.push(rk(function () { return wiggle(Math.max(F[k], 0), A[k], 1, 0.5, u - 2 * L)[k]; }));\n" +
        "    }\n" +
        "}\n" +
        "if (rk_wait) { value; } else {\n" +
        "    var r = value - v0;\n" +
        "    for (var j = 0; j < " + D + "; j++) r[j] = r[j] + (1 - e) * w1[j] + e * w2[j];\n" +
        "    r;\n" +
        "}";
    return s;
};

// Returns the effect group's name.
RK.auto.applyWiggle = function (prop, separate) {
    var layer = RK.layerOf(prop), comp = layer.containingComp;
    var D = separate ? RK.auto.dims(prop) : 1;
    return RK.auto.undo("Wiggle", function () {
        var g = RK.uniqueEffectName(layer, prop.name + " Wiggle");
        RK.addGroup(layer, g, RK.auto.wiggleParams(D, comp.duration));
        RK.auto.expr(prop, RK.auto.wiggleExpr(g, D));
        return g;
    });
};

RK.auto.wigglePage = {
    id: "wigglePage", label: "Wiggle", icon: RK.auto.I.wiggle,
    page: function (body) {
        var st = RK.auto.st;
        RK.ui.radios(body, ["Separate Dimensions", "All Dimensions"], st.wiggleSep ? 0 : 1, {
            vertical: true,
            tips: ["One amplitude and one frequency for each dimension", "One amplitude and one frequency for all dimensions"],
            onChange: function (i) { st.wiggleSep = i === 0; }
        });
        RK.ui.bottomRow(body, [RK.ui.backItem("Cancel"), {
            label: "Wiggle", icon: RK.I.ok, name: "ok", tip: "Creates the wiggle on the selected property",
            onClick: RK.ui.action("Wiggle", function () {
                var c = RK.auto.current();
                if (!c || !RK.auto.needNumeric(c, "Wiggle")) return false;
                RK.auto.applyWiggle(c.prop, st.wiggleSep);
            }, true)
        }]);
    }
};

RK.tool("wiggle", {
    label: "Wiggle", icon: RK.auto.I.wiggle,
    tip: "Creates a looping wiggle on the selected property (the noise repeats every Loop Duration). On 2D and 3D values, choose one wiggle for all dimensions or one per dimension.",
    run: function () {
        var c = RK.auto.current();
        if (!c || !RK.auto.needNumeric(c, "Wiggle")) return;
        if (RK.auto.dims(c.prop) > 1) {
            RK.ui.openPage(RK.auto.wigglePage);
            return;
        }
        RK.auto.applyWiggle(c.prop, false);
    }
});

// ---------------------------------------------------------------------------- Swing (§3.2)

// out = V(t) + A sin^(n)(2 pi F (t + O frames)), n = ceil(|Damping|) extra sines.
RK.auto.swingExpr = function (g) {
    return RK.auto.head("swing") + RK.auto.xMe +
        RK.auto.xfx("A", "me", RK.fxg(g, "Amplitude")) + RK.auto.xfx("F", "me", RK.fxg(g, "Frequency")) +
        RK.auto.xfx("O", "me", RK.fxg(g, "Offset")) + RK.auto.xfx("Dm", "me", RK.fxg(g, "Damping")) +
        RK.auto.ADD +
        "if (rk_wait) { value; } else {\n" +
        "    var s = Math.sin(2 * Math.PI * F * (time + O * thisComp.frameDuration));\n" +
        "    var n = Math.min(Math.ceil(Math.abs(Dm)), 1000);\n" +
        "    for (var i = 0; i < n; i++) s = Math.sin(s);\n" +
        "    rkAdd(value, A * s);\n" +
        "}";
};

RK.auto.applySwing = function (prop) {
    var layer = RK.layerOf(prop);
    return RK.auto.undo("Swing", function () {
        var g = RK.uniqueEffectName(layer, prop.name + " Swing");
        RK.addGroup(layer, g, [
            { name: "Amplitude", type: "slider", value: 10 },
            { name: "Frequency", type: "slider", value: 1 },
            { name: "Offset", type: "slider", value: 0 },
            { name: "Damping", type: "slider", value: 0 }
        ]);
        RK.auto.expr(prop, RK.auto.swingExpr(g));
        return g;
    });
};

RK.tool("swing", {
    label: "Swing", icon: RK.auto.I.swing,
    tip: "Creates an oscillation on the selected property (Amplitude, Frequency in Hz, Offset in frames, Damping lingers at the extremes).",
    run: function () {
        var c = RK.auto.oneLayerProp("swing");
        if (!c || !RK.auto.needNumeric(c, "Swing")) return;
        RK.auto.applySwing(c.prop);
    }
});

// ---------------------------------------------------------------------------- Spring (§3.3)

RK.auto.isMainPosition = function (p) {
    if (p.matchName !== "ADBE Position") return false;
    try {
        return p.propertyDepth === 2;
    } catch (e) {
        return true;
    }
};

// Light spring: once the keyframed motion has (almost) stopped at or after a key m > 1, the
// value overshoots: o = V'(t0 - frame) (0.15 / f) sin(2 pi f tau) exp(-d tau), faded in as the
// speed drops below the trigger speed. Bounce (1D) keeps the offset on the side the value came
// from.
RK.auto.springExpr = function (g, bounce) {
    return RK.auto.head("spring") + RK.auto.xMe +
        RK.auto.xfx("f", "me", RK.fxg(g, "Elasticity")) + RK.auto.xfx("d", "me", RK.fxg(g, "Damping")) +
        RK.auto.xfx("T", "me", RK.fxg(g, "Trigger speed")) +
        (bounce ? RK.auto.xfx("B", "me", RK.fxg(g, "Bounce")) : "var B = 0;\n") +
        "var dt = thisComp.frameDuration, n = numKeys, m = 0, t0 = 0, vt = null, v0 = null, p0 = null, pb = null;\n" +
        "if (n >= 2) {\n" +
        "    m = nearestKey(time).index;\n" +
        "    if (m > 1) {\n" +
        "        t0 = key(m).time <= time ? key(m).time : key(m - 1).time;\n" +
        "        vt = rk(function () { return velocityAtTime(time); });\n" +
        "        v0 = rk(function () { return velocityAtTime(t0 - dt); });\n" +
        (bounce ? "        p0 = rk(function () { return valueAtTime(t0); });\n        pb = rk(function () { return valueAtTime(t0 - dt); });\n" : "") +
        "    }\n" +
        "}\n" +
        "if (rk_wait || n < 2 || m <= 1 || f === 0) { value; } else {\n" +
        "    var sp = length(vt);\n" +
        "    if (sp > T) { value; } else {\n" +
        "        var tau = time - t0;\n" +
        "        var o = v0 * (0.15 / f * Math.sin(2 * Math.PI * f * tau) * Math.exp(-d * tau));\n" +
        "        if (T > 0) o = o * (1 - sp / T);\n" +
        (bounce ? "        if (B) { pb > p0 ? value + Math.abs(o) : (pb < p0 ? value - Math.abs(o) : value); } else { value + o; }\n" :
            "        value + o;\n") +
        "    }\n" +
        "}";
};

// Simulated spring on a layer's Position, from its motion in the world (parents included).
// The world velocity is (P(t) - P(t - 0.01)) * 100; frames are scanned back from now for the
// latest rest (exact zero speed) and the last moving frame before it. While moving, the layer
// trails its motion by w * velocity (faded in over R frames after the rest); at rest it
// overshoots with the velocity of the last moving frame. The scan (RK.auto.SPRING) asks for its
// frames in two batches, so a long scan costs three runs, not one per frame.
RK.auto.springSimExpr = function (g) {
    return RK.auto.head("spring") + RK.auto.xMe +
        RK.auto.xfx("f", "me", RK.fxg(g, "Elasticity")) + RK.auto.xfx("d", "me", RK.fxg(g, "Damping")) +
        "var par = me == null ? null : rk(function () { return rkParent(me); });\n" +
        "var dt = thisComp.frameDuration, S = null;\n" +
        "if (par != null) {\n" +
        "    " + RK.auto.xCompile(RK.auto.SPRING_FN, ["rp", "rx", "rw", "n", "dt", "now"], RK.auto.SPRING) +
        // the layer's own animation (pre-expression) placed by its parent
        "    S = " + RK.auto.SPRING_FN + "(\n" +
        "        function (t) { return rkF ? __h(__VALUE, thisComp.__c, me, \"transform/position\", t, true) : valueAtTime(t); },\n" +
        "        function (t) { return par === false ? false : rkXfAt(par, t); },\n" +
        "        function (X, p, t) { return X === false ? p : rkWt(X, p, t); },\n" +
        "        timeToFrames(time), dt, time);\n" +
        "    if (S === null) rk_wait = true;\n" +
        "}\n" +
        "var o = null;\n" +
        "if (!rk_wait && S !== null) {\n" +
        "    var ff = f === 0 ? 0.1 : f, dd = d === 0 ? 0.1 : d;\n" +
        "    var R = ff / dd, w = 1 / (10 * dd), k = (time - S[0] * dt) / dt, tau = time - S[1] * dt;\n" +
        "    var h = w * (k <= R ? k / R : 1);\n" +
        "    o = [-S[2] * h, -S[3] * h];\n" +
        "    if (S[2] === 0 && S[3] === 0) {\n" +
        "        var c = 0.15 / ff * Math.sin(2 * Math.PI * ff * tau) * Math.exp(-dd * tau);\n" +
        "        o = [o[0] + S[4] * c, o[1] + S[5] * c];\n" +
        "    }\n" +
        "    if (par !== false) {\n" +
        "        var X = rk(function () { return rkXfAt(par, time); });\n" +
        "        var q = X === null ? null : rk(function () { return rkFv(X, [o[0], o[1], 0], time); });\n" +
        "        if (q !== null) o = [q[0], q[1]];\n" +
        "    }\n" +
        "}\n" +
        "if (rk_wait || o === null) { value; } else { value + o; }";
};

RK.auto.applySpring = function (prop, simulated) {
    var layer = RK.layerOf(prop);
    var oneD = RK.dims(prop) === 1;
    var sim = !!simulated && RK.auto.isMainPosition(prop);
    return RK.auto.undo("Spring", function () {
        var g = RK.uniqueEffectName(layer, prop.name + " Spring");
        var params = [{ name: "Elasticity", type: "slider", value: 10 }, { name: "Damping", type: "slider", value: 5 }];
        if (oneD) params.push({ name: "Bounce", type: "checkbox", value: false });
        params.push({ name: "Trigger speed", type: "slider", value: 0.1 });
        RK.addGroup(layer, g, params);
        RK.auto.expr(prop, sim ? RK.auto.springSimExpr(g) : RK.auto.springExpr(g, oneD));
        return g;
    });
};

RK.auto.springPage = {
    id: "springPage", label: "Spring", icon: RK.auto.I.spring,
    page: function (body) {
        var st = RK.auto.st, W = RK.PAGE_W;
        var text = function (s) {
            var t = RK.wrap(s, 46);
            return RK.ui.label(body, t, W);
        };
        text("On a layer's Position, the spring can be computed in two ways:");
        RK.ui.separator(body);
        var light = RK.auto.left(RK.ui.radio(body, "Without simulation - Light", !st.springSim, "Bounces when the layer's own animation stops at a keyframe"));
        text("Requires the layer itself to be animated.");
        RK.ui.separator(body);
        var sim = RK.auto.left(RK.ui.radio(body, "Simulated - Heavy computing", st.springSim, "Follows the layer's motion in the world, parents included"));
        text("Also works without animation on the layer, using its parents' movement.\nThe result can also be smoother.");
        light.onClick = function () { st.springSim = false; light.value = true; sim.value = false; };
        sim.onClick = function () { st.springSim = true; sim.value = true; light.value = false; };
        RK.ui.bottomRow(body, [RK.ui.backItem("Cancel"), {
            label: "Spring", icon: RK.I.ok, name: "ok", tip: "Spring",
            onClick: RK.ui.action("Spring", function () {
                var c = RK.auto.current();
                if (!c || !RK.auto.needNumeric(c, "Spring")) return false;
                RK.auto.applySpring(c.prop, st.springSim);
            }, true)
        }]);
    }
};

RK.tool("spring", {
    label: "Spring", icon: RK.auto.I.spring,
    tip: "The property bounces like a spring when its animation stops. On a layer's Position, you can also simulate it from the layer's motion in the world.",
    run: function () {
        var c = RK.auto.current();
        if (!c || !RK.auto.needNumeric(c, "Spring")) return;
        if (RK.auto.isMainPosition(c.prop)) {
            RK.ui.openPage(RK.auto.springPage);
            return;
        }
        RK.auto.applySpring(c.prop, false);
    }
});

// ---------------------------------------------------------------------------- Blink (§3.4)

// Each cycle of P = fps / Frequency frames starts "on" (the property's own value) for Time on
// percent of it, then shows the Off value(s).
RK.auto.blinkExpr = function (g, D, color) {
    var s = RK.auto.head("blink") + RK.auto.xMe +
        RK.auto.xfx("F", "me", RK.fxg(g, "Frequency")) + RK.auto.xfx("On", "me", RK.fxg(g, "Time on")) +
        RK.auto.xfx("O", "me", RK.fxg(g, "Offset")), off;
    if (color || D === 1) {
        s += RK.auto.xfx("X0", "me", RK.fxg(g, "Off value"));
        off = "X0";
    } else {
        var ax = ["X", "Y", "Z"], v = [];
        for (var k = 0; k < D; k++) {
            s += RK.auto.xfx(ax[k] + "0", "me", RK.fxg(g, ax[k] + " Off value"));
            v.push(ax[k] + "0");
        }
        off = "[" + v.join(", ") + "]";
    }
    return s +
        "if (rk_wait) { value; } else {\n" +
        "    var P = F > 0 ? timeToFrames(1) / F : 0;\n" +
        "    var c = timeToFrames(time) + O;\n" +
        "    P > 0 && c % P > P * On / 100 ? " + off + " : value;\n" +
        "}";
};

RK.auto.applyBlink = function (prop) {
    var layer = RK.layerOf(prop);
    var color = RK.auto.isColor(prop), D = RK.auto.dims(prop);
    return RK.auto.undo("Blink", function () {
        var g = RK.uniqueEffectName(layer, prop.name + " Blink");
        var params = [
            { name: "Frequency", type: "slider", value: 1 },
            { name: "Time on", type: "slider", value: 50 },
            { name: "Offset", type: "slider", value: 0 }
        ];
        if (color) params.push({ name: "Off value", type: "color", value: [0, 0, 0, 1] });
        else if (D === 1) params.push({ name: "Off value", type: "slider", value: 0 });
        else for (var k = 0; k < D; k++) params.push({ name: ["X", "Y", "Z"][k] + " Off value", type: "slider", value: 0 });
        RK.addGroup(layer, g, params);
        RK.auto.expr(prop, RK.auto.blinkExpr(g, D, color));
        return g;
    });
};

RK.tool("blink", {
    label: "Blink", icon: RK.auto.I.blink,
    tip: "Makes the selected property blink between its own value and an Off value (Frequency in blinks per second, Time on in percent, Offset in frames).",
    run: function () {
        var c = RK.auto.oneLayerProp("blink");
        if (!c || !RK.auto.needNumeric(c, "Blink")) return;
        RK.auto.applyBlink(c.prop);
    }
});

// ---------------------------------------------------------------------------- Orient to path (§3.5)

// Rotation = V(t) + the heading of the motion between one frame before and one frame after
// (held at the first and last keys), atan2 of the Position's path: 0 when moving right.
RK.auto.orientExpr = function (separated) {
    var keys = function (id, api) {
        return "rkKeys(me, \"transform/" + id + "\", function (L) { var p = L.transform." + api +
            ", t = []; for (var i = 1; i <= p.numKeys; i++) t.push(p.key(i).time); return t; })";
    };
    var at = function (id, api) {
        return "rk(function () { return rkAt(me, \"transform/" + id + "\", t, function (L, t) { return L.transform." + api + ".valueAtTime(t); }); })";
    };
    var s = RK.auto.head("orient to path") + RK.auto.xMe;
    if (!separated) {
        s += "var K = me == null ? null : rk(function () { return " + keys("position", "position") + "; });\n" +
            "function rkP(t) { return " + at("position", "position") + "; }\n";
    } else {
        s += "var KX = me == null ? null : rk(function () { return " + keys("positionX", "xPosition") + "; });\n" +
            "var KY = me == null ? null : rk(function () { return " + keys("positionY", "yPosition") + "; });\n" +
            "var K = KX === null || KY === null ? null : KX.concat(KY).sort(function (a, b) { return a - b; });\n" +
            "function rkP(t) { var x = " + at("positionX", "xPosition") + ", y = " + at("positionY", "yPosition") + "; return x === null || y === null ? null : [x, y]; }\n";
    }
    return s +
        "var A = null, B = null, dt = thisComp.frameDuration;\n" +
        // between the first and last keys (most of the time) these are the samples: ask now
        "if (me != null) { rkP(time - dt); rkP(time + dt); }\n" +
        "if (K !== null && K.length >= 2) {\n" +
        "    var t1 = K[0], t2 = K[K.length - 1];\n" +
        "    if (time < t1) { A = rkP(t1); B = rkP(t1 + dt); }\n" +
        "    else if (time > t2) { A = rkP(t2 - dt); B = rkP(t2); }\n" +
        "    else { A = rkP(time - dt); B = rkP(time + dt); }\n" +
        "}\n" +
        "if (rk_wait || K === null || K.length < 2) { value; } else {\n" +
        "    var dx = B[0] - A[0], dy = B[1] - A[1];\n" +
        "    value + (dx === 0 && dy === 0 ? 0 : Math.atan2(dy, dx) * 180 / Math.PI);\n" +
        "}";
};

RK.auto.applyOrient = function (layer) {
    var pos = layer.transform.position;
    var sep = false;
    try {
        sep = !!pos.dimensionsSeparated;
    } catch (e) {}
    return RK.auto.undo("Orient to path", function () {
        RK.auto.expr(RK.rotProp(layer), RK.auto.orientExpr(sep));
    });
};

RK.tool("orient", {
    label: "Orient to path", icon: RK.auto.I.orient,
    tip: "Orients the selected layer along its motion path (from its Position keyframes). Its own rotation is kept as an offset.",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var sel = comp.selectedLayers;
        if (sel.length !== 1) {
            RK.msg(sel.length ? "Select only one layer to orient along its path." : "No layer selected: select the layer to orient along its path.");
            return;
        }
        RK.auto.applyOrient(sel[0]);
    }
});

// ---------------------------------------------------------------------------- Wheel (§3.6)

// Horizontal: Rotation = V(t) + s * (180 / pi) * X / R, X the world x of the anchor point.
// Curved: the same with the signed distance rolled frame by frame from the layer's in point
// (or the comp start): + while moving right, - otherwise.
RK.auto.wheelExpr = function (g, curved) {
    var s = RK.auto.head("wheel") + RK.auto.xMe +
        RK.auto.xfx("R", "me", RK.fxg(g, "Radius")) + RK.auto.xfx("rev", "me", RK.fxg(g, "Reverse"));
    if (!curved) {
        return s + RK.auto.xWorldReads("W", "me") +
            "if (rk_wait) { value; } else {\n" +
            RK.auto.xWorld("W") +
            "    value + (R > 0 ? (rev ? -1 : 1) * W[0] * 180 / Math.PI / R : 0);\n" +
            "}";
    }
    return s +
        "var par = me == null ? null : rk(function () { return rkParent(me); });\n" +
        "var D = null;\n" +
        "if (par != null) {\n" +
        "    " + RK.auto.xCompile(RK.auto.ROLL_FN, ["rp", "rx", "rw", "n0", "n1", "dt"], RK.auto.ROLL) +
        "    D = " + RK.auto.ROLL_FN + "(\n" +
        "        function (t) { return rkAt(me, \"transform/position\", t, function (L, t) { return L.transform.position.valueAtTime(t); }); },\n" +
        "        function (t) { return par === false ? false : rkXfAt(par, t); },\n" +
        "        function (X, p, t) { return X === false ? p : rkWt(X, p, t); },\n" +
        "        timeToFrames(Math.max(thisLayer.inPoint, 0)), timeToFrames(Math.min(time, thisLayer.outPoint)), thisComp.frameDuration);\n" +
        "    if (D === null) rk_wait = true;\n" +
        "}\n" +
        "if (rk_wait) { value; } else { value + (R > 0 ? (rev ? -1 : 1) * D * 180 / Math.PI / R : 0); }";
};

RK.auto.applyWheel = function (layer, radius, curved) {
    return RK.auto.undo("Wheel", function () {
        var g = RK.uniqueEffectName(layer, "Wheel");
        RK.addGroup(layer, g, [{ name: "Radius", type: "slider", value: radius }, { name: "Reverse", type: "checkbox", value: false }]);
        RK.auto.expr(RK.rotProp(layer), RK.auto.wheelExpr(g, curved));
        return g;
    });
};

// Distance between the two selected layers' anchor points, rounded (null after a message).
RK.auto.measure = function () {
    var comp = RK.comp();
    if (!comp) return null;
    var sel = comp.selectedLayers;
    if (sel.length !== 2) {
        RK.msg("Select two layers to measure the distance between them (the wheel and a layer on its rim).");
        return null;
    }
    return RK.auto.keepSelection(comp, function () {
        return RK.auto.undo("Measure", function () {
            return Math.round(RK.dist(RK.auto.worldPos(sel[0]), RK.auto.worldPos(sel[1])));
        });
    });
};

RK.auto.runWheel = function () {
    var st = RK.auto.st;
    var r = RK.auto.parse(st.wheelRadius);
    if (!(r > 0)) {
        RK.msg("Invalid radius: type the radius of the wheel in pixels (more than 0), or measure it.");
        return false;
    }
    var comp = RK.comp();
    if (!comp) return false;
    var sel = comp.selectedLayers;
    if (!sel.length) {
        RK.msg("No layer selected: select the wheel layer.");
        return false;
    }
    RK.auto.applyWheel(sel[0], r, st.wheelCurved);
};

RK.tool("wheel", {
    label: "Wheel", icon: RK.auto.I.wheel,
    tip: "Rotates the selected layer like a wheel rolling as it moves.",
    page: function (body) {
        var st = RK.auto.st;
        // Rigging > Measure leaves its last distance for this field
        if (typeof RK.shared.wheelRadius === "number") {
            st.wheelRadius = String(RK.shared.wheelRadius);
            RK.shared.wheelRadius = null;
        }
        var row = RK.ui.row(body, "Radius:");
        var field = RK.ui.number(row, st.wheelRadius, 50, "Radius of the wheel, in pixels");
        field.onChanging = function () { st.wheelRadius = this.text; };
        field.onChange = field.onChanging;
        RK.ui.button(row, "Measure", function () {
            var d = RK.auto.measure();
            if (d === null) return;
            st.wheelRadius = String(d);
            field.text = st.wheelRadius;
        }, "Measures the radius: select the wheel and a layer on its rim (a null), then click Measure");
        RK.ui.label(body, "Movement:");
        RK.ui.radios(body, ["Horizontal", "Curved"], st.wheelCurved ? 1 : 0, {
            vertical: true,
            tips: ["Light: exact on flat, horizontal ground", "Follows any trajectory; costlier, the longer the composition the more"],
            onChange: function (i) { st.wheelCurved = i === 1; }
        });
        RK.ui.bottomRow(body, [RK.ui.backItem("Cancel"), {
            label: "Wheel", icon: RK.I.ok, name: "ok", tip: "Wheel",
            onClick: RK.ui.action("Wheel", RK.auto.runWheel, true)
        }]);
    }
});

// ---------------------------------------------------------------------------- Lens (§3.7)

// Opacity or Scale times one of the centre's Lens Flare percentages.
RK.auto.lensPctExpr = function (centreName, param) {
    var s = RK.auto.head("lens"), L = centreName === null ? "me" : "C";
    s += centreName === null ? RK.auto.xMe : RK.auto.xLayer("C", centreName);
    return s + RK.auto.xfx("k", L, RK.fxg("Lens Flare", param)) +
        "if (rk_wait) { value; } else { value * k / 100; }";
};

// An element on the line from the emitter (0 %) through the comp centre (50 %) to the mirrored
// point (100 %): V + (1 - i) C + i M, C the centre's world position, M = comp size - C.
RK.auto.lensPosExpr = function (centreName) {
    return RK.auto.head("lens") + RK.auto.xMe +
        RK.auto.xfx("di", "me", RK.fxg("Lens Flare", "Distance")) +
        RK.auto.xLayer("C", centreName) + RK.auto.xWorldReads("W", "C") +
        "if (rk_wait) { value; } else {\n" +
        RK.auto.xWorld("W") +
        "    var i = di / 100;\n" +
        "    value + (1 - i) * [W[0], W[1]] + i * [thisComp.width - W[0], thisComp.height - W[1]];\n" +
        "}";
};

RK.auto.applyLens = function (comp, centre, elements) {
    return RK.auto.undo("Lens", function () {
        RK.nameCheck(comp);
        var cn = centre.name, N = elements.length;
        RK.addGroup(centre, "Lens Flare", [{ name: "Intensity", type: "slider", value: 100 }, { name: "Scale", type: "slider", value: 100 }]);
        RK.auto.expr(centre.transform.opacity, RK.auto.lensPctExpr(null, "Intensity"));
        RK.auto.expr(centre.transform.scale, RK.auto.lensPctExpr(null, "Scale"));
        for (var k = 0; k < N; k++) {
            var L = elements[k], pos = L.transform.position;
            // keyframed positions stay: their keys act as offsets
            if (pos.numKeys === 0 && !pos.dimensionsSeparated) {
                RK.withUnlocked(L, function () { pos.setValue([0, 0, 0]); });
            }
            RK.addGroup(L, "Lens Flare", [{ name: "Distance", type: "slider", value: 100 * (k + 1) / N }]);
            RK.auto.expr(pos, RK.auto.lensPosExpr(cn));
            RK.auto.expr(L.transform.opacity, RK.auto.lensPctExpr(cn, "Intensity"));
            RK.auto.expr(L.transform.scale, RK.auto.lensPctExpr(cn, "Scale"));
        }
        RK.selectOnly(comp, [centre]);
    });
};

RK.tool("lens", {
    label: "Lens", icon: RK.auto.I.lens,
    tip: "Creates a lens flare with the selected layers: select the centre (the light) first, then the flare elements.",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var sel = comp.selectedLayers;
        if (sel.length < 2) {
            RK.msg("Select all the lens flare layers, starting with the centre (the light).");
            return;
        }
        var sep = RK.auto.separated(sel.slice(1));
        if (sep) {
            RK.msg("The Position of " + sep.name + " has separated dimensions: join them first (Lens drives the whole Position).");
            return;
        }
        RK.auto.applyLens(comp, sel[0], sel.slice(1));
    }
});

// ---------------------------------------------------------------------------- Move away (§3.8)

// With a parent: slide the layer along the line from the parent's anchor point through it,
// by "Distance from parent" pixels (in the world): Q + (P - Q) (L + delta) / L, in the
// parent's space, L the world distance.
RK.auto.moveAwayExpr = function () {
    return RK.auto.head("move away") + RK.auto.xMe +
        RK.auto.xfx("dl", "me", "Distance from parent") +
        "var par = me == null ? null : rk(function () { return rkParent(me); });\n" +
        "var Q = par == null || par === false ? null : rk(function () { return rkVal(par, \"transform/anchor\", function (L) { return L.transform.anchorPoint.value; }); });\n" +
        "var X = par == null || par === false ? null : rk(function () { return rkXf(par); });\n" +
        "var dv = null;\n" +
        "if (!rk_wait && par !== false) {\n" +
        "    if (Q === null) Q = [0, 0, 0];\n" +
        "    dv = rk(function () { return rkWv(X, value - Q); });\n" +
        "}\n" +
        "if (rk_wait || par === false || dv === null) { value; } else {\n" +
        "    var L = length(dv);\n" +
        "    if (L > 0) { var r = Q + (value - Q) * ((L + dl) / L); r.slice(0, value.length); } else { value; }\n" +
        "}";
};

RK.auto.applyMoveAway = function (layers) {
    return RK.auto.undo("Move away", function () {
        for (var i = 0; i < layers.length; i++) {
            var L = layers[i];
            // the slider is shared by later runs instead of piling up unused copies
            if (!RK.findEffect(L, "Distance from parent")) {
                var e = RK.addSlider(L, "Distance from parent", 0);
                RK.fxOff(L, e);
            }
            RK.auto.expr(L.transform.position, RK.auto.moveAwayExpr());
        }
        return layers.length;
    });
};

RK.tool("moveaway", {
    label: "Move away", icon: RK.auto.I.moveaway,
    tip: "Adds a simple control (Distance from parent) to move the selected layers away from their parents, or closer.",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var sel = comp.selectedLayers;
        if (!sel.length) {
            RK.msg("No layer selected: select the layers to move away from their parents.");
            return;
        }
        var sep = RK.auto.separated(sel);
        if (sep) {
            RK.msg("The Position of " + sep.name + " has separated dimensions: join them first (Move away drives the whole Position).");
            return;
        }
        RK.auto.applyMoveAway(sel);
    }
});

// ---------------------------------------------------------------------------- Distance link (§3.9)

// The property keeps its value while the distance to the reference stays between Minimum and
// Maximum; outside, it changes by one unit per Falloff pixels (the other way with Reverse).
RK.auto.distLinkExpr = function (g, refName, ownPos) {
    return RK.auto.head("distance link") + RK.auto.xMe +
        RK.auto.xfx("a", "me", RK.fxg(g, "Minimum Distance")) + RK.auto.xfx("b", "me", RK.fxg(g, "Maximum Distance")) +
        RK.auto.xfx("rv", "me", RK.fxg(g, "Reverse")) + RK.auto.xfx("fo", "me", RK.fxg(g, "Falloff")) +
        RK.auto.xLayer("R", refName) +
        RK.auto.xWorldReads("Tw", "me", ownPos ? "value" : null) + RK.auto.xWorldReads("Rw", "R") +
        RK.auto.ADD +
        "if (rk_wait) { value; } else {\n" +
        RK.auto.xWorld("Tw") + RK.auto.xWorld("Rw") +
        "    var d = length(Rw, Tw), lo = Math.abs(a), hi = Math.abs(b), s = rv ? -1 : 1, o = 0;\n" +
        "    if (hi >= lo && fo !== 0) {\n" +
        "        if (hi === 0) o = s * d / fo;\n" +
        "        else if (d > hi) o = s * (d - hi) / fo;\n" +
        "        else if (d < lo) o = s * (lo - d) / fo;\n" +
        "    }\n" +
        "    rkAdd(value, o);\n" +
        "}";
};

RK.auto.applyDistLink = function (comp, target, prop, ref) {
    return RK.auto.undo("Distance link", function () {
        var d0 = RK.auto.keepSelection(comp, function () { return RK.dist(RK.auto.worldPos(target), RK.auto.worldPos(ref)); });
        var g = RK.uniqueEffectName(target, prop.name + " Distance Link");
        RK.addGroup(target, g, [
            { name: "Minimum Distance", type: "slider", value: 0.75 * d0 },
            { name: "Maximum Distance", type: "slider", value: 1.25 * d0 },
            { name: "Reverse", type: "checkbox", value: false },
            { name: "Falloff", type: "slider", value: 10 }
        ]);
        RK.nameCheck(comp);
        var own = prop.matchName === "ADBE Position" && RK.auto.isMainPosition(prop);
        RK.auto.expr(prop, RK.auto.distLinkExpr(g, ref.name, own));
        return g;
    });
};

RK.tool("distlink", {
    label: "Dist. link", icon: RK.auto.I.distlink,
    tip: "Links a property to the distance from another layer: select the property to animate, then the reference layer.",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var sel = comp.selectedLayers;
        if (sel.length !== 2) {
            RK.msg("Select the property to animate, then the reference layer (two layers).");
            return;
        }
        var props = sel[0].selectedProperties;
        var p = props.length ? props[props.length - 1] : null;
        if (!p) {
            RK.msg("No property selected: select the property to animate on the first layer.");
            return;
        }
        if (!RK.canExpr(p)) {
            RK.msg("The selected property (" + p.name + ") can't hold an expression, so it can't be rigged.");
            return;
        }
        if (!RK.auto.needNumeric({ prop: p }, "Distance link")) return;
        RK.auto.applyDistLink(comp, sel[0], p, sel[1]);
    }
});

// ---------------------------------------------------------------------------- Exposure (§3.10)

// Fixed: the property is sampled every E frames and held in between.
RK.auto.exposureExpr = function (g) {
    return RK.auto.head("exposure") + RK.auto.xMe +
        RK.auto.xfx("E", "me", RK.fxg(g, "Exposure (frames)")) +
        "var v = null;\n" +
        "if (!rk_wait) {\n" +
        "    if (E === 0) E = 1;\n" +
        "    var f = timeToFrames(time);\n" +
        "    var tt = (f - f % E) * thisComp.frameDuration;\n" +
        "    v = rk(function () { return valueAtTime(tt); });\n" +
        "}\n" +
        "if (rk_wait) { value; } else { v; }";
};

RK.auto.applyExposureFixed = function (prop) {
    var layer = RK.layerOf(prop);
    return RK.auto.undo("Exposure", function () {
        var g = RK.uniqueEffectName(layer, prop.name + " Exposure");
        RK.addGroup(layer, g, [{ name: "Exposure (frames)", type: "slider", value: 1 }]);
        RK.auto.expr(prop, RK.auto.exposureExpr(g));
        return g;
    });
};

// Animated numeric properties among the selected properties of the layers: [{layer, props}].
RK.auto.animatedProps = function (layers) {
    var out = [];
    for (var i = 0; i < layers.length; i++) {
        var ps = RK.auto.layerProps(layers[i]), keep = [];
        for (var j = 0; j < ps.length; j++) {
            var p = ps[j];
            if (p.numKeys > 0 && RK.auto.numeric(p)) keep.push(p);
        }
        out.push({ layer: layers[i], props: keep });
    }
    return out;
};

// Every key of the properties to Hold, in one command (it acts in the active comp only, and
// refuses keys of locked layers: they are unlocked for it).
RK.auto.holdAll = function (comp, props) {
    var keys = [], locked = [], seen = {};
    for (var i = 0; i < props.length; i++) {
        var p = props[i], L = RK.layerOf(p), n = p.numKeys;
        if (!seen[L.id]) {
            seen[L.id] = true;
            if (L.locked) locked.push(L);
        }
        for (var k = 0; k < n; k++) keys.push({ layer: L.id, prop: p.__uid, index: k });
    }
    if (!keys.length) return;
    for (i = 0; i < locked.length; i++) locked[i].locked = false;
    try {
        app.run("keys.interpolation", { comp: comp.id, "in": "hold", out: "hold", continuous: false, autoBezier: false, __keys: keys });
    } finally {
        for (i = 0; i < locked.length; i++) locked[i].locked = true;
    }
};

// Keys (comp time, value) added to properties, values given (read before any key is added,
// so the new keys sample the animation as it was).
RK.auto.addKeys = function (adds) {
    for (var i = 0; i < adds.length; i++) {
        var a = adds[i];
        RK.withUnlocked(RK.layerOf(a.prop), function () { a.prop.setValueAtTime(a.time, a.value); });
    }
};

// Adaptative exposure (keys + Hold). o: {lower, upper, precision, sync, all}. A new drawing
// (key) is made when the value has moved by 10000 / precision since the last drawing, or after
// `upper` frames, never sooner than every `lower` frames, never while the value is still.
// Returns {keys, props}.
RK.auto.exposureAdaptive = function (comp, layers, o) {
    var m = Math.round(o.lower), M = Math.round(o.upper), prec = o.precision;
    if (!(m > 0)) m = 1;
    if (!(M > 0)) M = 1;
    if (!prec) prec = 1;
    var theta = 10000 / prec;
    var dt = comp.frameDuration, nf = Math.round(comp.duration / dt);
    var per = RK.auto.animatedProps(layers), groups = [], all = [], i, j;
    for (i = 0; i < per.length; i++) {
        for (j = 0; j < per[i].props.length; j++) all.push(per[i].props[j]);
        if (o.sync && !o.all && per[i].props.length) groups.push(per[i].props);
    }
    if (!all.length) return { keys: 0, props: 0 };
    if (o.sync && o.all) groups = [all];
    else if (!o.sync) for (i = 0; i < all.length; i++) groups.push([all[i]]);
    var steps = [];
    for (var n = 0; n * m < nf; n++) steps.push(n * m * dt);
    var adds = [];
    for (var gi = 0; gi < groups.length; gi++) {
        var grp = groups[gi], info = [];
        for (i = 0; i < grp.length; i++) {
            var p = grp[i], kt = [], kv = [], vals = [];
            for (var k = 1; k <= p.numKeys; k++) {
                kt.push(p.keyTime(k));
                kv.push(p.keyValue(k));
            }
            for (var s = 0; s < steps.length; s++) vals.push(p.valueAtTime(steps[s], true));
            info.push({ prop: p, kt: kt, kv: kv, vals: vals });
        }
        var c = 0;
        for (s = 0; s < steps.length; s++) {
            var tau = steps[s], g = 0;
            c += m;
            for (i = 0; i < info.length; i++) {
                var f = info[i], last = f.kt.length - 1;
                if (tau <= f.kt[0] + 1e-6 || tau >= f.kt[last] - 1e-6) continue;
                var b = 0;
                while (b + 1 <= last && f.kt[b + 1] <= tau + 1e-6) b++;
                g = Math.max(g, RK.auto.diff(f.vals[s], f.kv[b]));
            }
            if ((g >= theta || c >= M) && g > 0) {
                for (i = 0; i < info.length; i++) {
                    var fi = info[i];
                    adds.push({ prop: fi.prop, time: tau, value: fi.vals[s] });
                    // a held drawing: later changes are measured from it
                    var at = 0;
                    while (at < fi.kt.length && fi.kt[at] < tau - 1e-6) at++;
                    if (at < fi.kt.length && Math.abs(fi.kt[at] - tau) <= 1e-6) fi.kv[at] = fi.vals[s];
                    else {
                        fi.kt.splice(at, 0, tau);
                        fi.kv.splice(at, 0, fi.vals[s]);
                    }
                }
                c = 0;
            }
            if (g === 0) c = 0;
        }
    }
    RK.auto.undo("Exposure", function () {
        RK.auto.addKeys(adds);
        RK.auto.holdAll(comp, all);
    });
    return { keys: adds.length, props: all.length };
};

// Precision such that the threshold is the average speed (property units per frame) times the
// average exposure: floor(10000 / (s (lower + upper) / 2)); 0 without any measurable speed.
RK.auto.detectPrecision = function (comp, layers, lower, upper) {
    var m = Math.round(lower), M = Math.round(upper);
    if (!(m > 0)) m = 1;
    if (!(M > 0)) M = 1;
    var dt = comp.frameDuration, per = RK.auto.animatedProps(layers), sum = 0, n = 0;
    for (var i = 0; i < per.length; i++) {
        for (var j = 0; j < per[i].props.length; j++) {
            var p = per[i].props[j];
            var f0 = Math.max(0, Math.round(p.keyTime(1) / dt)), f1 = Math.round(p.keyTime(p.numKeys) / dt);
            var tot = 0, cnt = 0, prev = p.valueAtTime(f0 * dt, true);
            for (var f = f0 + 1; f <= f1; f++) {
                var v = p.valueAtTime(f * dt, true);
                tot += RK.auto.diff(v, prev);
                cnt++;
                prev = v;
            }
            if (cnt && tot / cnt > 0) {
                sum += tot / cnt;
                n++;
            }
        }
    }
    if (!n) return 0;
    return Math.floor(10000 / (sum / n * (m + M) / 2));
};

// The image measure of the footage analysis, as a colour expression: on the layer at `index`, a
// 10 x 10 grid of boxes (half size a * layer size / 20, a = accuracy 0-1) is averaged on the
// layer's own image (masks and effects included); the sum of the 100 averages / 20, i.e. five
// times their mean colour.
RK.auto.footageCode = function (index, a) {
    return RK.xHead("exposure analysis") +
        "var L = rk(function () { return thisComp.layer(" + index + "); });\n" +
        "var S = L === null ? null : rk(function () { return [L.width, L.height]; });\n" +
        "var sum = [0, 0, 0, 0];\n" +
        "if (S !== null) {\n" +
        "    var W = S[0], H = S[1], r = [" + a + " * W / 20, " + a + " * H / 20];\n" +
        "    for (var i = 1; i <= 10; i++) for (var j = 1; j <= 10; j++) {\n" +
        "        var c = rk(function () { return L.sampleImage([(i - 0.5) * W / 10, (j - 0.5) * H / 10], r, true, time); });\n" +
        "        if (c !== null) sum = [sum[0] + c[0], sum[1] + c[1], sum[2] + c[2], sum[3] + c[3]];\n" +
        "    }\n" +
        "}\n" +
        "if (rk_wait) { [0, 0, 0, 0]; } else { [sum[0] / 20, sum[1] / 20, sum[2] / 20, sum[3] / 20]; }";
};

// Footage analysis: the frames where the layer's image changes, measured by RK.auto.footageCode
// on a temporary probe at each frame (sampleImage takes a time: the comp's current time doesn't
// move). A frame is a new drawing when a checked channel moved by more than tolerance / 1000
// from the last drawing. o: {accuracy, tolerance (0-100), channels: [r, g, b, a]}. Returns the
// times (comp seconds), the in point first.
RK.auto.analyzeFootage = function (comp, layer, o) {
    var dt = comp.frameDuration;
    var t0 = Math.max(layer.inPoint, 0), t1 = Math.min(layer.outPoint, comp.duration);
    var a = Math.max(0, o.accuracy) / 100, tol = o.tolerance / 1000, ch = o.channels;
    var times = [];
    RK.auto.keepSelection(comp, function () { RK.auto.undo("Exposure analysis", function () {
        var tmp = comp.layers.addNull();
        try {
            var fx = RK.addEffect(tmp, "ADBE Color Control", "rk probe");
            RK.setExpr(fx.property(1), RK.auto.footageCode(layer.index, a));
            var ref = null;
            for (var f = Math.round(t0 / dt); f * dt < t1 - 1e-6; f++) {
                var t = f * dt, v = fx.property(1).valueAtTime(t, false);
                if (ref === null) {
                    ref = v;
                    times.push(t);
                    continue;
                }
                for (var k = 0; k < 4; k++) {
                    if (ch[k] && Math.abs(v[k] - ref[k]) > tol) {
                        ref = v;
                        times.push(t);
                        break;
                    }
                }
            }
        } finally {
            tmp.remove();
        }
    }); });
    RK.auto.footage = { name: layer.name, times: times };
    return times;
};

// Keys at the analysed times on the selected properties (animated or not), then Hold.
RK.auto.exposureFootage = function (comp, layers, times) {
    var props = [], adds = [];
    for (var i = 0; i < layers.length; i++) {
        var ps = RK.auto.layerProps(layers[i]);
        for (var j = 0; j < ps.length; j++) {
            var p = ps[j];
            var ok = false;
            try {
                ok = p.canVaryOverTime;
            } catch (e) {}
            if (!ok) continue;
            props.push(p);
            for (var k = 0; k < times.length; k++) adds.push({ prop: p, time: times[k], value: p.valueAtTime(times[k], true) });
        }
    }
    if (!props.length) return { keys: 0, props: 0 };
    RK.auto.undo("Exposure", function () {
        RK.auto.addKeys(adds);
        RK.auto.holdAll(comp, props);
    });
    return { keys: adds.length, props: props.length };
};

// The Exposure button: runs the chosen mode on the selection.
RK.auto.runExposure = function () {
    var st = RK.auto.st.exp;
    var comp = RK.comp();
    if (!comp) return false;
    var layers = comp.selectedLayers;
    if (!layers.length) {
        RK.msg("No layer selected: select the animated properties to change.");
        return false;
    }
    var r;
    if (st.mode === 0) {
        var c = RK.auto.current();
        if (!c || !RK.auto.needNumeric(c, "Exposure")) return false;
        RK.auto.applyExposureFixed(c.prop);
        return;
    }
    if (st.mode === 1) {
        r = RK.auto.exposureAdaptive(comp, layers, {
            lower: RK.ui.num({ text: st.lower }, 1), upper: RK.ui.num({ text: st.upper }, 4),
            precision: RK.ui.num({ text: st.precision }, 1), sync: st.sync, all: st.syncAll
        });
        if (!r.props) {
            RK.msg("Select animated properties (with keyframes) to change their exposure.");
            return false;
        }
    } else {
        if (!RK.auto.footage) {
            RK.msg("Analyze a layer first (From Footage > Analyze).");
            return false;
        }
        r = RK.auto.exposureFootage(comp, layers, RK.auto.footage.times);
        if (!r.props) {
            RK.msg("Select the properties to change.");
            return false;
        }
    }
    RK.msg("Exposure: " + r.keys + " keyframe" + (r.keys === 1 ? "" : "s") + " set on " + r.props + " propert" + (r.props === 1 ? "y" : "ies") + ", all held.");
};

RK.auto.refreshLayers = function () {
    var st = RK.auto.st.exp, comp = RK.comp(true), items = [];
    if (comp) for (var i = 1; i <= comp.numLayers; i++) items.push(i + " - " + comp.layer(i).name);
    st.layerItems = items;
    if (st.layerSel >= items.length) st.layerSel = -1;
    return items;
};

RK.auto.runAnalyze = function () {
    var st = RK.auto.st.exp;
    var comp = RK.comp();
    if (!comp) return null;
    var item = st.layerSel >= 0 ? st.layerItems[st.layerSel] : null;
    var idx = item ? parseInt(item, 10) : NaN;
    if (!(idx >= 1 && idx <= comp.numLayers)) {
        RK.msg("Choose the layer to analyze in the list (refresh it first if needed).");
        return null;
    }
    var layer = comp.layer(idx);
    var times = RK.auto.analyzeFootage(comp, layer, {
        accuracy: RK.ui.num({ text: st.accuracy }, 50, 0, 100),
        tolerance: RK.ui.num({ text: st.tolerance }, 10, 0, 100),
        channels: st.chan
    });
    st.status = "Analyzed - " + layer.name + " (" + times.length + " drawing" + (times.length === 1 ? "" : "s") + ")";
    return times;
};

RK.tool("exposure", {
    label: "Exposure", icon: RK.auto.I.exposure,
    tip: "Changes the exposure of the animation of the selected properties: fixed (every n frames), adaptative (following the speed), or detected from footage.",
    page: function (body) {
        var st = RK.auto.st.exp, lw = 108, W = RK.PAGE_W;
        var adaptCtl = [], footCtl = [];
        var setAll = function (list, on) { for (var i = 0; i < list.length; i++) list[i].enabled = on; };
        var fixed = RK.auto.left(RK.ui.radio(body, "Fixed", st.mode === 0, "Holds every n-th frame: an expression with its own control, removable"));
        RK.ui.separator(body);
        var adapt = RK.auto.left(RK.ui.radio(body, "Adaptative", st.mode === 1, "Adds keyframes where the motion needs a new drawing, then holds every keyframe"));
        var r1 = RK.ui.row(body, "Lower exp. limit:", lw);
        var eLow = RK.ui.number(r1, st.lower, 50, "Never a new drawing sooner than this many frames");
        var r2 = RK.ui.row(body, "Upper exp. limit:", lw);
        var eUp = RK.ui.number(r2, st.upper, 50, "A new drawing at least every this many frames while it moves");
        var r3 = RK.ui.row(body, "Precision:", lw);
        var ePrec = RK.ui.number(r3, st.precision, 50, "The higher, the smaller the motion that makes a new drawing (threshold 10000 / precision)");
        var sPrec = body.add("slider", undefined, Math.max(0, Math.min(1000, RK.ui.num({ text: st.precision }, 500))), 0, 1000);
        sPrec.preferredSize = [W, 20];
        var bDetect = RK.ui.button(r3, "Detect", function () {
            var comp = RK.comp();
            if (!comp) return;
            var p = RK.auto.detectPrecision(comp, comp.selectedLayers, RK.ui.num(eLow, 1), RK.ui.num(eUp, 4));
            st.precision = String(p);
            ePrec.text = st.precision;
            sPrec.value = Math.max(0, Math.min(1000, p));
            if (!p) RK.msg("No motion to measure: select animated properties.");
        }, "Sets the precision from the average speed of the selected animated properties");
        var r4 = RK.ui.row(body);
        var cSync = RK.ui.check(r4, "Sync", st.sync, "Keys at the same times on several properties");
        var rSync = RK.ui.radios(r4, ["By layer", "All"], st.syncAll ? 1 : 0, {
            tips: ["Each layer's properties together", "All the selected properties together"],
            onChange: function (i) { st.syncAll = i === 1; }
        });
        adaptCtl.push(eLow, eUp, ePrec, bDetect, sPrec, cSync);
        RK.ui.separator(body);
        var foot = RK.auto.left(RK.ui.radio(body, "From Footage", st.mode === 2, "Detects the exposure of existing footage (a hand-drawn animation...)"));
        var r5 = RK.ui.row(body, RK.ui.expert ? "" : "Layer:", 44);
        var dLayer = RK.ui.drop(r5, st.layerItems.length ? st.layerItems : [""], Math.max(0, st.layerSel), 190, "The layer to analyze");
        if (st.layerSel < 0) dLayer.selection = null;
        var refresh = function () {
            var items = RK.auto.refreshLayers();
            dLayer.removeAll();
            for (var i = 0; i < items.length; i++) dLayer.add("item", items[i]);
            dLayer.selection = st.layerSel >= 0 ? st.layerSel : null;
        };
        dLayer.onChange = function () { st.layerSel = this.selection ? this.selection.index : -1; };
        var bRefresh = RK.ui.flat(r5, { icon: RK.auto.I.refresh, tip: "Refresh the list of layers", w: 30, h: 22, iconW: 20, center: true, name: "refresh", onClick: refresh });
        var slide = function (label, value, svalue, tip, key, skey) {
            var r = RK.ui.row(body, label, 64);
            var s = r.add("slider", undefined, svalue, 0, 100);
            s.preferredSize = [168, 20];
            s.helpTip = tip;
            var e = RK.ui.number(r, value, 44, tip);
            s.onChanging = function () {
                st[skey] = this.value;
                st[key] = String(Math.round(this.value));
                e.text = st[key];
            };
            s.onChange = s.onChanging;
            e.onChanging = function () { st[key] = this.text; };
            footCtl.push(s, e);
        };
        slide("Accuracy", st.accuracy, st.accSlider, "Lower accuracy makes the analysis quicker", "accuracy", "accSlider");
        slide("Tolerance", st.tolerance, st.tolSlider, "Higher tolerance gives longer detected exposures", "tolerance", "tolSlider");
        var r8 = RK.ui.row(body);
        var chan = [];
        var names = ["R", "G", "B", "A"];
        for (var k = 0; k < 4; k++) {
            (function (k) {
                var c = RK.ui.check(r8, names[k], st.chan[k], "Detect changes in the " + ["red", "green", "blue", "alpha"][k] + " channel");
                c.onClick = function () { st.chan[k] = this.value; };
                chan.push(c);
            })(k);
        }
        var r9 = RK.ui.row(body);
        var status;
        var bAnalyze = RK.ui.button(r9, "Analyze", function () {
            var t = RK.auto.runAnalyze();
            if (t) status.text = st.status;
        }, "Analyzes the chosen layer frame by frame (the panel waits until it is done)");
        status = RK.ui.label(body, st.status, W);
        footCtl.push(dLayer, bRefresh, bAnalyze, status);
        for (k = 0; k < 4; k++) footCtl.push(chan[k]);
        var mode = function (m) {
            st.mode = m;
            fixed.value = m === 0;
            adapt.value = m === 1;
            foot.value = m === 2;
            setAll(adaptCtl, m === 1);
            rSync.setEnabled(m === 1 && st.sync);
            setAll(footCtl, m === 2);
        };
        fixed.onClick = function () { mode(0); };
        adapt.onClick = function () { mode(1); };
        foot.onClick = function () {
            mode(2);
            refresh();
        };
        cSync.onClick = function () {
            st.sync = this.value;
            rSync.setEnabled(st.mode === 1 && st.sync);
        };
        eLow.onChanging = function () { st.lower = this.text; };
        eUp.onChanging = function () { st.upper = this.text; };
        ePrec.onChanging = function () {
            st.precision = this.text;
            var v = RK.auto.parse(this.text);
            if (!isNaN(v)) sPrec.value = Math.max(0, Math.min(1000, v));
        };
        sPrec.onChanging = function () {
            st.precision = String(Math.round(this.value));
            ePrec.text = st.precision;
        };
        sPrec.onChange = sPrec.onChanging;
        mode(st.mode);
        RK.ui.bottomRow(body, [RK.ui.backItem(), {
            label: "Exposure", icon: RK.I.ok, name: "ok", tip: "Sets the animation exposure",
            onClick: RK.ui.action("Exposure", RK.auto.runExposure, true)
        }]);
    }
});

// ---------------------------------------------------------------------------- Paint rigging (§3.11)

// The layer's first Paint effect, or null.
RK.auto.paintFx = function (layer) {
    var fx = RK.effects(layer);
    if (!fx) return null;
    for (var i = 1; i <= fx.numProperties; i++) if (fx.property(i).matchName === "ec.paint.paint") return fx.property(i);
    return null;
};

// A Paint effect's strokes (brushes, clone strokes, erasers), oldest first.
RK.auto.strokes = function (paint) {
    var out = [];
    for (var i = 1; i <= paint.numProperties; i++) {
        var s = paint.property(i);
        if (s.propertyType !== PropertyType.PROPERTY && s.property("Stroke Options")) out.push(s);
    }
    return out;
};

RK.auto.strokeOptions = function (s) {
    return s.property("Stroke Options");
};
RK.auto.strokeTransform = function (s) {
    for (var i = 1; i <= s.numProperties; i++) {
        var p = s.property(i);
        if (p.propertyType !== PropertyType.PROPERTY && p.property("Anchor Point") && p.property("Rotation")) return p;
    }
    return null;
};

// Stroke k of N (k = 1 the oldest): Start/End = N * percent - 100 (k - 1), so the strokes are
// written on (End) or erased (Start) one after the other in drawing order. N is the Paint
// effect's current number of strokes (its children minus `extra`, its own parameters).
RK.auto.paintRigExpr = function (which, paintName, extra, k) {
    var s = RK.auto.head("paint rigging") + RK.auto.xMe + RK.auto.xfx("v", "me", RK.fxg("Paint Rig", which));
    if (which === "Diameter") return s + "if (rk_wait) { value; } else { value + v; }";
    return s + "var n = me == null ? null : rk(function () { return rkKids(me, " + RK.q(paintName) +
        ", function (L) { return L.effect(" + RK.q(paintName) + ").numProperties; }); });\n" +
        "if (rk_wait) { value; } else { (n - " + extra + ") * v - " + 100 * (k - 1) + "; }";
};

// Rigs every stroke of the first Paint effect of each layer. Returns {layers, strokes}.
RK.auto.paintRig = function (layers) {
    var res = { layers: 0, strokes: 0 };
    RK.auto.undo("Paint rigging", function () {
        for (var i = 0; i < layers.length; i++) {
            var L = layers[i], paint = RK.auto.paintFx(L);
            if (!paint) continue;
            var list = RK.auto.strokes(paint);
            var extra = paint.numProperties - list.length, name = paint.name;
            RK.addGroup(L, "Paint Rig", [
                { name: "Start", type: "slider", value: 0 },
                { name: "End", type: "slider", value: 100 },
                { name: "Diameter", type: "slider", value: 0 }
            ]);
            for (var k = 0; k < list.length; k++) {
                var so = RK.auto.strokeOptions(list[k]);
                RK.auto.expr(so.property("Start"), RK.auto.paintRigExpr("Start", name, extra, k + 1));
                RK.auto.expr(so.property("End"), RK.auto.paintRigExpr("End", name, extra, k + 1));
                RK.auto.expr(so.property("Diameter"), RK.auto.paintRigExpr("Diameter", name, extra, k + 1));
            }
            res.layers++;
            res.strokes += list.length;
        }
    });
    return res;
};

RK.tool("paintrig", {
    label: "Paint rigging", icon: RK.auto.I.paintrig,
    tip: "Rigs the paint strokes of the selected layers (or of all layers) so that they write on and off one after the other, like a single stroke.",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var layers = comp.selectedLayers;
        if (!layers.length) {
            layers = [];
            for (var i = 1; i <= comp.numLayers; i++) layers.push(comp.layer(i));
        }
        var r = RK.auto.paintRig(layers);
        if (!r.layers) RK.msg("No Paint effect found on " + (comp.selectedLayers.length ? "the selected layers." : "the layers of this composition."));
        else RK.msg("Paint rigging: " + r.strokes + " stroke" + (r.strokes === 1 ? "" : "s") + " rigged on " + r.layers + " layer" + (r.layers === 1 ? "." : "s."));
    }
});

// ---------------------------------------------------------------------------- Paint group (§3.12)

// The strokes the selection designates on a layer: selected strokes, the strokes of selected
// properties inside strokes (the Timeline selects one group row at a time, but property rows
// add up with Shift), or every stroke of a selected Paint effect. Selection order, no repeats.
RK.auto.selectedStrokes = function (layer) {
    var sel = layer.selectedProperties, out = [], seen = {};
    var add = function (s) {
        if (!seen[s.__uid]) {
            seen[s.__uid] = true;
            out.push(s);
        }
    };
    for (var i = 0; i < sel.length; i++) {
        var p = sel[i];
        if (p.matchName === "ec.paint.paint") {
            var all = RK.auto.strokes(p);
            for (var k = 0; k < all.length; k++) add(all[k]);
            continue;
        }
        // climb to the child of a Paint effect
        var q = p, up = q.parentProperty, n = 0;
        while (up && !(up instanceof Layer) && up.matchName !== "ec.paint.paint" && n++ < 8) {
            q = up;
            up = q.parentProperty;
        }
        if (up && !(up instanceof Layer) && up.matchName === "ec.paint.paint" && q.propertyType !== PropertyType.PROPERTY && q.property("Stroke Options")) add(q);
    }
    return out;
};

RK.auto.rot = function (v, deg) {
    var a = deg * Math.PI / 180, c = Math.cos(a), s = Math.sin(a);
    return [v[0] * c - v[1] * s, v[0] * s + v[1] * c];
};

// Groups strokes under one control: shared Start, End, Color and Diameter offset, and one
// transform (Anchor Point placed at Position, Scale, Rotation) applied to all of them as one
// rigid drawing. Each stroke keeps where it is when grouped: stroke j's own transform becomes
// the group's composed with what it had relative to the first stroke. Returns the group name.
RK.auto.paintGroup = function (layer, strokes) {
    var first = strokes[0], t1 = RK.auto.strokeTransform(first);
    var A1 = t1.property("Anchor Point").value, P1 = t1.property("Position").value;
    var S1 = t1.property("Scale").value, R1 = t1.property("Rotation").value;
    var s1 = Math.abs(S1[0]) > 1e-9 ? S1[0] / 100 : 1;
    var color = null;
    for (var i = 0; i < strokes.length && !color; i++) {
        var c = RK.auto.strokeOptions(strokes[i]).property("Color");
        if (c) color = c.value;
    }
    // the stroke's transform relative to the group's (constants, read before any change)
    var rel = [];
    for (i = 0; i < strokes.length; i++) {
        var tr = RK.auto.strokeTransform(strokes[i]);
        var Aj = tr.property("Anchor Point").value, Pj = tr.property("Position").value;
        var Sj = tr.property("Scale").value, Rj = tr.property("Rotation").value;
        var sx = Math.abs(Sj[0]) > 1e-9 ? Sj[0] / 100 : 1, sy = Math.abs(Sj[1]) > 1e-9 ? Sj[1] / 100 : 1;
        // anchor = C + N (groupAnchor - A1), C = Aj - Sj^-1 Rot(-Rj) (Pj - P1),
        // N = s1 Sj^-1 Rot(R1 - Rj)
        var d = RK.auto.rot([Pj[0] - P1[0], Pj[1] - P1[1]], -Rj);
        var ex = RK.auto.rot([1, 0], R1 - Rj), ey = RK.auto.rot([0, 1], R1 - Rj);
        rel.push({
            tr: tr,
            C: [Aj[0] - d[0] / sx, Aj[1] - d[1] / sy],
            N: [s1 * ex[0] / sx, s1 * ey[0] / sx, s1 * ex[1] / sy, s1 * ey[1] / sy],
            dR: Rj - R1,
            k: [sx / s1, sy / s1]
        });
    }
    return RK.auto.undo("Paint group", function () {
        var g = RK.uniqueEffectName(layer, first.name + " Paint Group");
        RK.addGroup(layer, g, [
            { name: "Start", type: "slider", value: 0 },
            { name: "End", type: "slider", value: 100 },
            { name: "Color", type: "color", value: color || [1, 0, 0, 1] },
            { name: "Diameter", type: "slider", value: 0 },
            { name: "Anchor Point", type: "point", value: [A1[0], A1[1]] },
            { name: "Position", type: "point", value: [P1[0], P1[1]] },
            { name: "Scale", type: "slider", value: S1[0] },
            { name: "Rotation", type: "angle", value: R1 }
        ]);
        var x = function (param, result) {
            return RK.auto.head("paint group") + RK.auto.xMe + RK.auto.xfx("v", "me", RK.fxg(g, param)) +
                "if (rk_wait) { value; } else { " + result + "; }";
        };
        // full precision: the constants place strokes exactly where they were
        var n = function (v) {
            var r = Math.round(v * 1e9) / 1e9;
            return String(r === 0 ? 0 : r);
        };
        for (var j = 0; j < strokes.length; j++) {
            var so = RK.auto.strokeOptions(strokes[j]), r = rel[j], tr = r.tr;
            RK.auto.expr(so.property("Start"), x("Start", "v"));
            RK.auto.expr(so.property("End"), x("End", "v"));
            if (so.property("Color")) RK.auto.expr(so.property("Color"), x("Color", "v"));
            RK.auto.expr(so.property("Diameter"), x("Diameter", "value + v"));
            RK.auto.expr(tr.property("Position"), x("Position", "v"));
            RK.auto.expr(tr.property("Rotation"), x("Rotation", Math.abs(r.dR) < 1e-9 ? "v" : "v + " + n(r.dR)));
            RK.auto.expr(tr.property("Scale"), x("Scale", "[v * " + n(r.k[0]) + ", v * " + n(r.k[1]) + "]"));
            var ident = Math.abs(r.N[0] - 1) < 1e-9 && Math.abs(r.N[1]) < 1e-9 && Math.abs(r.N[2]) < 1e-9 && Math.abs(r.N[3] - 1) < 1e-9 &&
                Math.abs(r.C[0] - A1[0]) < 1e-6 && Math.abs(r.C[1] - A1[1]) < 1e-6;
            RK.auto.expr(tr.property("Anchor Point"), x("Anchor Point", ident ? "v" :
                "[" + n(r.C[0]) + " + " + n(r.N[0]) + " * (v[0] - " + n(A1[0]) + ") + " + n(r.N[1]) + " * (v[1] - " + n(A1[1]) + "), " +
                n(r.C[1]) + " + " + n(r.N[2]) + " * (v[0] - " + n(A1[0]) + ") + " + n(r.N[3]) + " * (v[1] - " + n(A1[1]) + ")]"));
        }
        return g;
    });
};

RK.tool("paintgroup", {
    label: "Paint group", icon: RK.auto.I.paintgroup,
    tip: "Groups the selected paint strokes (or every stroke of a selected Paint effect) so they can be animated as if there were only one: shared start, end, colour, diameter and transform.",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var sel = comp.selectedLayers;
        if (!sel.length) {
            RK.msg("No layer selected: select the paint strokes to group.");
            return;
        }
        var strokes = RK.auto.selectedStrokes(sel[0]);
        if (!strokes.length) {
            RK.msg("Select the paint strokes to group (or a property of each, with Shift), or the Paint effect to group all its strokes.");
            return;
        }
        RK.auto.paintGroup(sel[0], strokes);
    }
});

// ---------------------------------------------------------------------------- Randomize (§3.13)

// A key's full state, to put it back at another time.
RK.auto.keyState = function (p, i) {
    var s = {
        value: p.keyValue(i), inI: p.keyInInterpolationType(i), outI: p.keyOutInterpolationType(i),
        inE: p.keyInTemporalEase(i), outE: p.keyOutTemporalEase(i),
        cont: p.keyTemporalContinuous(i), auto: p.keyTemporalAutoBezier(i), spatial: false
    };
    if (p.isSpatial) {
        s.spatial = true;
        s.sin = p.keyInSpatialTangent(i);
        s.sout = p.keyOutSpatialTangent(i);
        s.scont = p.keySpatialContinuous(i);
        s.sauto = p.keySpatialAutoBezier(i);
        s.rov = p.keyRoving(i);
    }
    return s;
};

RK.auto.putKey = function (p, t, s) {
    p.setValueAtTime(t, s.value);
    var i = p.nearestKeyIndex(t);
    // eases first (they make the key Bezier), the interpolation types after
    try {
        p.setTemporalEaseAtKey(i, s.inE, s.outE);
    } catch (e) {}
    try {
        p.setInterpolationTypeAtKey(i, s.inI, s.outI);
    } catch (e2) {}
    if (s.cont) p.setTemporalContinuousAtKey(i, true);
    if (s.auto) p.setTemporalAutoBezierAtKey(i, true);
    if (s.spatial) {
        try {
            p.setSpatialTangentsAtKey(i, s.sin, s.sout);
            if (s.sauto) p.setSpatialAutoBezierAtKey(i, true);
            else if (s.scont) p.setSpatialContinuousAtKey(i, true);
            if (s.rov) p.setRovingAtKey(i, true);
        } catch (e3) {}
    }
};

// Randomizes the selection. o: {props, start, inP, outP, times, values (booleans), ranges:
// [[min, max] or null for X, Y, Z], fromCurrent}. Values are in property units, times in
// frames. An axis without a range leaves that component as it is. Returns a summary text, or
// null after a message.
RK.auto.randomize = function (comp, o) {
    var layers = comp.selectedLayers, dt = comp.frameDuration, i, j, k;
    if (!layers.length) {
        RK.msg("No layer selected: select what to randomize.");
        return null;
    }
    var R = o.ranges;
    var rnd = function (r) { return r[0] + Math.random() * (r[1] - r[0]); };
    // a new value: each component with a range becomes random (plus the current one with
    // "From current value"); colours use X, Y, Z for red, green, blue
    var newValue = function (cur, p) {
        var D = RK.dims(p), arr = cur instanceof Array;
        if (!arr) return R[0] ? (o.fromCurrent ? cur : 0) + rnd(R[0]) : cur;
        var v = cur.slice(), n = Math.min(D === 4 ? 3 : D, 3);
        for (var c = 0; c < n; c++) if (R[c]) v[c] = (o.fromCurrent ? cur[c] : 0) + rnd(R[c]);
        return v;
    };
    var count = 0;
    if (o.props) {
        var work = [];
        for (i = 0; i < layers.length; i++) {
            var ps = RK.auto.layerProps(layers[i]);
            if (!ps.length) {
                RK.msg("Select the properties you want to randomize (on every selected layer).");
                return null;
            }
            for (j = 0; j < ps.length; j++) if (RK.auto.numeric(ps[j])) work.push(ps[j]);
        }
        RK.auto.undo("Randomize", function () {
            for (var w = 0; w < work.length; w++) {
                var p = work[w];
                var v = newValue(p.valueAtTime(comp.time, true), p);
                RK.withUnlocked(RK.layerOf(p), function () {
                    if (p.numKeys > 0) p.setValueAtTime(comp.time, v);
                    else p.setValue(v);
                });
                count++;
            }
        });
        return count + " propert" + (count === 1 ? "y" : "ies") + " randomized.";
    }
    if (o.start || o.inP || o.outP) {
        if (!R[0]) {
            RK.msg("Define a randomizing range: X Min and X Max, in frames.");
            return null;
        }
        RK.auto.undo("Randomize", function () {
            for (var l = 0; l < layers.length; l++) {
                var L = layers[l];
                RK.withUnlocked(L, function () {
                    if (o.start) L.startTime = (o.fromCurrent ? L.startTime : 0) + rnd(R[0]) * dt;
                    if (o.inP) L.inPoint = (o.fromCurrent ? L.inPoint : 0) + rnd(R[0]) * dt;
                    if (o.outP) L.outPoint = (o.fromCurrent ? L.outPoint : 0) + rnd(R[0]) * dt;
                });
                count++;
            }
        });
        return count + " layer" + (count === 1 ? "" : "s") + " randomized.";
    }
    if (!o.values && !o.times) return null;
    var keyed = [];
    for (i = 0; i < layers.length; i++) {
        var lp = RK.auto.layerProps(layers[i]), any = false;
        for (j = 0; j < lp.length; j++) {
            var sk = lp[j].selectedKeys;
            if (sk.length) {
                keyed.push({ prop: lp[j], keys: sk });
                any = true;
            }
        }
        if (!any) {
            RK.msg("Select the keyframes you want to randomize (on every selected layer).");
            return null;
        }
    }
    if (o.times && !R[0]) {
        RK.msg("Define a randomizing range: X Min and X Max, in frames.");
        return null;
    }
    RK.auto.undo("Randomize", function () {
        for (var q = 0; q < keyed.length; q++) {
            var p = keyed[q].prop, ks = keyed[q].keys;
            RK.withUnlocked(RK.layerOf(p), function () {
                if (o.values && RK.auto.numeric(p)) {
                    for (var a = 0; a < ks.length; a++) p.setValueAtKey(ks[a], newValue(p.keyValue(ks[a]), p));
                }
                if (o.times) {
                    var moves = [], refs = [];
                    for (var b = 0; b < ks.length; b++) {
                        var t = p.keyTime(ks[b]);
                        moves.push({ time: (o.fromCurrent ? t : 0) + rnd(R[0]) * dt, state: RK.auto.keyState(p, ks[b]) });
                        refs.push({ layer: RK.layerOf(p).id, prop: p.__uid, index: ks[b] - 1 });
                    }
                    // all the moved keys go first (one command), then come back at their new times
                    app.run("keys.delete", { comp: comp.id, __keys: refs });
                    for (b = 0; b < moves.length; b++) RK.auto.putKey(p, moves[b].time, moves[b].state);
                }
            });
            count += ks.length;
        }
    });
    return count + " keyframe" + (count === 1 ? "" : "s") + " randomized.";
};

RK.auto.runRandomize = function () {
    var st = RK.auto.st.rnd;
    var comp = RK.comp();
    if (!comp) return false;
    var ranges = [];
    for (var k = 0; k < 3; k++) {
        var a = RK.auto.parse(st.min[k]), b = RK.auto.parse(st.max[k]);
        ranges.push(isNaN(a) || isNaN(b) ? null : [a, b]);
    }
    var r = RK.auto.randomize(comp, {
        props: st.props, start: st.start, inP: st.inP, outP: st.outP, times: st.times, values: st.values,
        ranges: ranges, fromCurrent: st.fromCurrent
    });
    if (r) RK.msg("Randomize: " + r);
    return false;
};

RK.tool("randomize", {
    label: "Randomize", icon: RK.auto.I.randomize,
    tip: "Randomizes the selected properties, the layers' timing or the selected keyframes.",
    page: function (body) {
        var st = RK.auto.st.rnd, c = {};
        c.props = RK.auto.left(RK.ui.check(body, "Selected properties", st.props, "Random values for the selected properties at the current time (a keyframe when animated)"));
        RK.ui.label(body, "Layers");
        var r1 = RK.ui.row(body);
        c.start = RK.ui.check(r1, "Start times", st.start, "Moves the selected layers in time (X, in frames)");
        c.inP = RK.ui.check(r1, "In points", st.inP, "Trims the in points (X, in frames)");
        c.outP = RK.ui.check(r1, "Out points", st.outP, "Trims the out points (X, in frames)");
        RK.ui.label(body, "Selected keyframes");
        var r2 = RK.ui.row(body);
        c.times = RK.ui.check(r2, "Times", st.times, "Moves the selected keyframes in time (X, in frames)");
        c.values = RK.ui.check(r2, "Values", st.values, "Random values for the selected keyframes");
        var keys = ["props", "start", "inP", "outP", "times", "values"];
        var sync = function () {
            var any = false;
            for (var i = 0; i < keys.length; i++) any = any || c[keys[i]].value;
            if (!any) c.props.value = true;
            for (i = 0; i < keys.length; i++) st[keys[i]] = c[keys[i]].value;
        };
        var off = function (list) { for (var i = 0; i < list.length; i++) c[list[i]].value = false; };
        c.props.onClick = function () { if (this.value) off(["start", "inP", "outP", "times", "values"]); sync(); };
        c.start.onClick = c.inP.onClick = c.outP.onClick = function () { if (this.value) off(["props", "times", "values"]); sync(); };
        c.times.onClick = c.values.onClick = function () { if (this.value) off(["props", "start", "inP", "outP"]); sync(); };
        var cols = body.add("group");
        cols.orientation = "row";
        cols.alignment = ["center", "top"];
        cols.alignChildren = ["center", "top"];
        cols.spacing = 12;
        var axis = ["X", "Y", "Z"];
        for (var k = 0; k < 3; k++) {
            (function (k) {
                var col = cols.add("group");
                col.orientation = "column";
                col.alignChildren = ["fill", "top"];
                col.spacing = 2;
                var lab = col.add("statictext", undefined, axis[k]);
                lab.preferredSize = [80, 16];
                lab.onDraw = function () {
                    this.graphics.drawString(axis[k], null, Math.round((80 - RK.textWidth(axis[k], RK.FONT)) / 2), 0);
                };
                var mk = function (which, label) {
                    var e = RK.ui.text(col, st[which][k], 80, label + " of the random range, " + axis[k] + " (frames for times)");
                    e.onChanging = function () { st[which][k] = this.text; };
                    var s = col.add("slider", undefined, st[which + "S"][k], -100, 100);
                    s.preferredSize = [70, 18];
                    s.alignment = ["center", "top"];
                    s.onChanging = function () {
                        st[which + "S"][k] = this.value;
                        st[which][k] = String(Math.round(this.value));
                        e.text = st[which][k];
                    };
                    s.onChange = s.onChanging;
                };
                mk("max", "Maximum");
                mk("min", "Minimum");
            })(k);
        }
        c.fromCurrent = RK.auto.left(RK.ui.check(body, "From current value", st.fromCurrent, "Adds the random value to the current one (unchecked: the random value replaces it)"));
        c.fromCurrent.onClick = function () { st.fromCurrent = this.value; };
        RK.ui.bottomRow(body, [RK.ui.backItem("Cancel"), {
            label: "Randomize", icon: RK.I.ok, name: "ok", tip: "Randomizes properties and layers",
            onClick: RK.ui.action("Randomize", RK.auto.runRandomize, false)
        }]);
    }
});

// ---------------------------------------------------------------------------- Time remap (§3.14)

// Time remapping whose last key sits on the layer's last frame (not one frame after it);
// loop: null, "out" (cycles after the last key) or "in" (before the first; the layer then
// starts at the comp start). Returns the number of layers changed.
RK.auto.timeRemap = function (comp, layers, loop) {
    var dt = comp.frameDuration, done = 0;
    RK.auto.undo("Time remap", function () {
        for (var i = 0; i < layers.length; i++) {
            var L = layers[i];
            var can = false;
            try {
                can = L.canSetTimeRemapEnabled;
            } catch (e) {}
            if (!can) continue;
            RK.withUnlocked(L, function () {
                var out = L.outPoint, last = out - dt;
                if (!L.timeRemapEnabled) L.timeRemapEnabled = true;
                var tr = L.property("ADBE Time Remapping");
                tr.setValueAtTime(last, tr.valueAtTime(last, true));
                // the key one frame after the last frame (the default end key) goes
                for (var k = tr.numKeys; k >= 1; k--) {
                    if (Math.abs(tr.keyTime(k) - out) < dt / 2) {
                        tr.removeKey(k);
                        break;
                    }
                }
                if (loop) {
                    tr.setValueAtTime(out, 0);
                    RK.setExpr(tr, RK.TAG + "time remap\nloop" + (loop === "in" ? "In" : "Out") + "(\"cycle\")");
                    if (loop === "in") L.inPoint = 0;
                }
            });
            done++;
        }
    });
    return done;
};

RK.auto.runTimeRemap = function () {
    var st = RK.auto.st.tr;
    var comp = RK.comp();
    if (!comp) return false;
    var sel = comp.selectedLayers;
    if (!sel.length) {
        RK.msg("No layer selected: select the footage or composition layers to remap.");
        return false;
    }
    var n = RK.auto.timeRemap(comp, sel, st.loop ? (st.loopIn ? "in" : "out") : null);
    if (!n) {
        RK.msg("Time remapping needs footage or composition layers.");
        return false;
    }
};

RK.tool("timeremap", {
    label: "Time remap", icon: RK.auto.I.timeremap,
    tip: "Time-remapping tools: better keyframes (the last one on the last frame), and seamless loops.",
    page: function (body) {
        var st = RK.auto.st.tr;
        var loop = RK.auto.left(RK.ui.check(body, "Loop", st.loop, "Loops the clip"));
        var how = RK.ui.radios(body, ["Loop out", "Loop in"], st.loopIn ? 1 : 0, {
            vertical: true,
            tips: ["Time remap with a loop after the last keyframe", "Time remap with a loop before the first keyframe"],
            onChange: function (i) { st.loopIn = i === 1; }
        });
        how.setEnabled(st.loop);
        loop.onClick = function () {
            st.loop = this.value;
            how.setEnabled(this.value);
        };
        RK.ui.bottomRow(body, [RK.ui.backItem(), {
            label: "Time Remap", icon: RK.I.ok, name: "ok", tip: "Smart time remap",
            onClick: RK.ui.action("Time remap", RK.auto.runTimeRemap, true)
        }]);
    }
});

// ---------------------------------------------------------------------------- the tab

RK.ui.addTab("automation", "Automation", RK.I.tabAuto, "Automation", function (page) {
    RK.ui.buildGrid(page, {
        left: ["wiggle", "spring", "orient", "lens", "distlink", "paintrig", "randomize"],
        right: ["swing", "blink", "wheel", "moveaway", "exposure", "paintgroup", "timeremap"]
    });
});

// ============================================================================ Animation tab
//
// Keyframe interpolation buttons, speed and influences (with presets), spatial interpolation,
// the Morpher, Copy / Paste anim, Cel animation (Paint strokes held for an exposure, with an
// onion skin) and Import rig in comp. The work is in plain RK.anim.* functions that take their
// options as arguments, so tests can call them; the tab and its two pages only read their
// controls and call them.

RK.anim = {};

// ---------------------------------------------------------------------------- icons (24 x 16)

RK.anim.I = {};
(function (I) {
    // a little standing figure centred on x, its head at y0
    var fig = function (x, y0) {
        y0 = y0 || 0;
        return [["C", x, y0 + 2.4, 1.9], ["L", x, y0 + 4.3, x, y0 + 9.3],
            ["L", x - 3.3, y0 + 6.6, x, y0 + 5.4, x + 3.3, y0 + 6.6], ["L", x - 2.8, y0 + 14.5, x, y0 + 9.3, x + 2.8, y0 + 14.5]];
    };
    var arrowRight = function (x0, x1, y) { return [["L", x0, y, x1, y], ["L", x1 - 3.4, y - 3.4, x1, y, x1 - 3.4, y + 3.4]]; };
    var onion = function (cx) {
        return [["E", cx, 10.2, 5.2, 4.8], ["E", cx, 10.2, 2.3, 4.8], ["L", cx - 2.6, 6, cx, 1, cx + 2.6, 6], ["L", cx - 1.6, 15.4, cx + 1.6, 15.4]];
    };

    // keyframe types: the standard keyframe glyphs
    I.roving = (function () {
        var ops = [["D", 12, 8, 2.6]];
        for (var i = 0; i < 8; i++) {
            var a = i * Math.PI / 4;
            ops.push(["D", 12 + 5.6 * Math.cos(a), 8 + 5.6 * Math.sin(a), 0.75]);
        }
        return ops;
    })();
    I.linear = [["P", 12, 1.5, 18.5, 8, 12, 14.5, 5.5, 8]];
    I.easeIn = [["P", 7, 1.5, 17.5, 8, 7, 14.5, 10.5, 8]];
    I.easeOut = [["P", 17, 1.5, 6.5, 8, 17, 14.5, 13.5, 8]];
    I.easyEase = [["P", 6.5, 1.5, 17.5, 1.5, 12, 8], ["P", 6.5, 14.5, 17.5, 14.5, 12, 8]];
    I.autoBezier = [["D", 12, 8, 5.6]];
    I.hold = [["F", 6.5, 2.5, 11, 11]];

    // speed: a value curve with its velocity handle; influences: a key with its handle
    I.speed = [RK.op("L", RK.bez(2, 14, 10, 14, 14, 3, 22, 3, 12)), ["L", 7.6, 12.6, 16.4, 4.4], ["D", 7.6, 12.6, 1.4], ["D", 16.4, 4.4, 1.4]];
    I.inInf = [["P", 18, 4, 22, 8, 18, 12, 14, 8], ["L", 14, 8, 2.5, 8], ["D", 2.5, 8, 1.6]];
    I.outInf = [["P", 6, 4, 10, 8, 6, 12, 2, 8], ["L", 10, 8, 17.5, 8], ["D", 17.5, 8, 1.6]];
    I.lockInf = [["L", 1.5, 11, 9.5, 11], ["D", 1.5, 11, 1.5], ["F", 11.5, 8, 10, 7.5], RK.op("L", RK.arc(16.5, 8, 3.2, 180, 360, 8))];
    I.get = [["P", 6, 3, 11, 8, 6, 13, 1, 8]].concat(arrowRight(13, 22, 8));

    // spatial interpolation: a pointed path, a curved path with its handles
    I.spLinear = [["L", 2, 13.5, 12, 3, 22, 13.5], ["D", 2, 13.5, 1.5], ["D", 12, 3, 1.9], ["D", 22, 13.5, 1.5]];
    I.spBezier = [RK.op("L", RK.bez(2, 13.5, 6, 2.5, 18, 2.5, 22, 13.5, 14)), ["L", 5, 5.6, 19, 5.6], ["D", 12, 5.6, 1.8],
        ["F", 3.5, 4.1, 3, 3], ["F", 17.5, 4.1, 3, 3], ["D", 2, 13.5, 1.3], ["D", 22, 13.5, 1.3]];

    // tools
    I.morpher = [RK.op("L", RK.bez(2.5, 13.5, 14, 13.5, 10, 2.5, 21.5, 2.5, 16)), ["D", 2.5, 13.5, 1.8], ["D", 21.5, 2.5, 1.8]];
    I.copyAnim = fig(5.5).concat(arrowRight(11, 22, 8));
    I.pasteAnim = fig(18.5).concat(arrowRight(1.5, 12.5, 8));
    I.cel = (function () {
        var rs = [6.0, 4.4, 5.6, 4.8, 6.3, 4.3, 5.4, 4.7, 6.1, 4.5, 5.7, 4.6], pts = [];
        for (var i = 0; i < rs.length; i++) {
            var a = i * 30 * Math.PI / 180;
            pts.push(10.5 + rs[i] * 1.1 * Math.cos(a), 8.3 + rs[i] * Math.sin(a));
        }
        return [RK.op("P", pts), ["D", 20.8, 3.2, 1.6], ["D", 21.5, 12.4, 1.1], ["D", 2.4, 14, 1.0], ["D", 3, 2.4, 0.8]];
    })();
    I.importRig = [["R", 11.5, 0.8, 11.5, 14.4]].concat(fig(17.25, 0.6)).concat(arrowRight(1, 10, 8));
    I.refresh = [RK.op("L", RK.arc(12, 8, 5.6, -50, 250, 16)), ["L", 14.2, 0.9, 15.6, 3.7, 12.5, 4.6]];
    I.duplicate = fig(4.2, 0.6).concat(fig(12, 0.6)).concat(fig(19.8, 0.6));
    I.onion = onion(12);
    I.getOnion = onion(17).concat(arrowRight(1, 9.5, 9));
})(RK.anim.I);

// ---------------------------------------------------------------------------- small helpers

RK.anim.KEY = { roving: "Roving", linear: "Linear Interpolation", easeIn: "Ease In", easeOut: "Ease Out",
    easyEase: "Easy Ease", autoBezier: "Auto Bezier", hold: "Hold" };

// Run a key command on exactly the keys `refs` names ([{layer, prop, index}], 0-based): the
// batched engine commands change any number of keys in one call and give the user's key
// selection back.
RK.anim.cmd = function (comp, id, params, refs) {
    if (!refs || !refs.length) return;
    params.comp = comp.id;
    params.__keys = refs;
    app.run(id, params);
};

RK.anim.is3D = function (L) {
    return L instanceof CameraLayer || L instanceof LightLayer || RK.is3D(L);
};

RK.anim.clamp = function (v, lo, hi) {
    return Math.max(lo, Math.min(hi, v));
};

// An influence typed in a field: a whole number 1-100, or `def` when the text isn't one.
RK.anim.influence = function (text, def) {
    var v = parseInt(String(text), 10);
    return isNaN(v) || v <= 0 ? def : Math.min(100, v);
};

// A message-safe wrapper for the tab's own buttons (tools get this from RK.ui.runTool).
RK.anim.guard = function (label, fn) {
    return function () {
        RK.clearMsg();
        try {
            fn.call(this);
        } catch (e) {
            RK.msg(label + ": " + RK.err(e));
        }
    };
};

// ---------------------------------------------------------------------------- presets
//
// Settings > Animation keeps the presets as "speed - in/out" texts (pref interpPresets, the
// "Presets" header first, as the dropdown shows them).

RK.anim.DEFAULT_PRESETS = ["0 - 33/33", "0 - 50/50", "0 - 80/80", "0 - 25/75", "0 - 15/85"];

RK.anim.presets = function () {
    var p = RK.pref("interpPresets", null), out = [];
    if (!(p instanceof Array)) p = RK.anim.DEFAULT_PRESETS;
    for (var i = 0; i < p.length; i++) {
        var s = RK.trim(p[i]);
        if (s !== "" && s !== "Presets") out.push(s);
    }
    return out;
};

RK.anim.savePresets = function (list) {
    RK.setPref("interpPresets", ["Presets"].concat(list));
};

// "speed - in/out" -> {speed, inf, outf}, or null when the text isn't a preset. A negative or
// decimal speed reads as typed.
RK.anim.parsePreset = function (s) {
    var m = /^\s*(-?\d+(?:[.,]\d+)?)\s*-\s*(\d+(?:[.,]\d+)?)\s*\/\s*(\d+(?:[.,]\d+)?)\s*$/.exec(String(s));
    if (!m) return null;
    var f = function (x) { return parseFloat(x.replace(",", ".")); };
    return { speed: f(m[1]), inf: f(m[2]), outf: f(m[3]) };
};

RK.anim.formatPreset = function (speed, inf, outf) {
    return RK.trim(speed) + " - " + RK.trim(inf) + "/" + RK.trim(outf);
};

// Add a preset: appended, the list sorted as text, saved. Returns the new list.
RK.anim.addPreset = function (text) {
    var list = RK.anim.presets();
    list.push(text);
    list.sort();
    RK.anim.savePresets(list);
    return list;
};

// Remove the preset at `i` (0-based, header not counted). Returns the new list.
RK.anim.removePreset = function (i) {
    var list = RK.anim.presets();
    if (i >= 0 && i < list.length) list.splice(i, 1);
    RK.anim.savePresets(list);
    return list;
};

// ---------------------------------------------------------------------------- keyframes

// The selected layers' selected properties that can be keyframed, with their keys and which of
// them are selected (0-based). Locked layers are left out: nothing may change them.
// {layers, props: [{layer, prop, info, keys, sel}], anySelected}
RK.anim.collect = function (comp) {
    var layers = comp.selectedLayers;
    var out = { layers: layers, props: [], anySelected: false };
    for (var i = 0; i < layers.length; i++) {
        var L = layers[i];
        if (L.locked) continue;
        var props = L.selectedProperties;
        for (var j = 0; j < props.length; j++) {
            var p = props[j];
            if (!(p instanceof Property)) continue;
            var info = p.__info();
            if (!info.canVaryOverTime || info.hidden) continue;
            var keys = info.numKeys ? p.__keys() : [];
            var sel = [];
            for (var k = 0; k < keys.length; k++) if (keys[k].selected) sel.push(k);
            if (sel.length) out.anySelected = true;
            out.props.push({ layer: L, prop: p, info: info, keys: keys, sel: sel });
        }
    }
    return out;
};

// A keyframe type button. With selected keys, they get the type; without, every selected
// property gets a key at the current time (or keeps the one there) with the type. Roving only
// concerns spatial properties (Position, Anchor Point, points). Eases use influences
// `inf` / `outf` (%) and speed 0. Returns the number of keys changed.
RK.anim.keyType = function (comp, type, inf, outf) {
    var c = RK.anim.collect(comp);
    if (!c.layers.length || !RK.anim.KEY[type]) return 0;
    var withKeys = c.anySelected, todo = [], i;
    for (i = 0; i < c.props.length; i++) {
        var e = c.props[i];
        if (type === "roving" ? !e.info.spatial : type !== "hold" && e.info.interpolates === false) continue;
        if (withKeys && !e.sel.length) continue;
        todo.push(e);
    }
    if (!todo.length) return 0;
    inf = RK.anim.clamp(inf || 33, 0.1, 100);
    outf = RK.anim.clamp(outf || 33, 0.1, 100);
    return RK.undo(RK.NAME + " - " + RK.anim.KEY[type], function () {
        var refs = [], spatial = [], t = comp.time;
        for (var a = 0; a < todo.length; a++) {
            var x = todo[a], idx = x.sel;
            if (!withKeys) idx = [x.prop.addKey(t) - 1];
            for (var k = 0; k < idx.length; k++) {
                var r = { layer: x.layer.id, prop: x.prop.__uid, index: idx[k] };
                refs.push(r);
                if (x.info.spatial) spatial.push(r);
            }
        }
        // Changing selected keys also takes them off roving (not keys made here); first, as a
        // roving key takes no ease or type.
        if (withKeys && type !== "roving") RK.anim.cmd(comp, "keys.interpolation", { roving: false }, spatial);
        RK.anim.applyType(comp, type, refs, inf, outf);
        return refs.length;
    });
};

RK.anim.applyType = function (comp, type, refs, inf, outf) {
    var cmd = function (id, p) { RK.anim.cmd(comp, id, p, refs); };
    // the ease first: it makes both sides Bezier; the types after it
    var ease = function () {
        cmd("keys.velocity", { inSpeed: 0, inInfluence: inf, outSpeed: 0, outInfluence: outf, continuous: false });
    };
    if (type === "roving") cmd("keys.interpolation", { roving: true });
    else if (type === "linear") cmd("keys.interpolation", { "in": "linear", out: "linear", continuous: false, autoBezier: false });
    else if (type === "easeIn") {
        ease();
        cmd("keys.interpolation", { "in": "bezier", out: "linear", continuous: false, autoBezier: false });
    } else if (type === "easeOut") {
        ease();
        cmd("keys.interpolation", { "in": "linear", out: "bezier", continuous: false, autoBezier: false });
    } else if (type === "easyEase") {
        ease();
        cmd("keys.interpolation", { "in": "bezier", out: "bezier", continuous: false, autoBezier: false });
    } else if (type === "autoBezier") {
        // EffectCraft clears Continuous when it sets Auto Bezier: set it again so both flags read
        // on, as for an auto-Bezier key in After Effects (the curve is the same).
        cmd("keys.interpolation", { continuous: true, autoBezier: true });
        cmd("keys.interpolation", { continuous: true });
    } else if (type === "hold") cmd("keys.interpolation", { "in": "hold", out: "hold", continuous: false, autoBezier: false });
};

// "Set": new eases on the selected keys. o: {speed, inf, outf}, each null to keep the key's own.
// The keys keep their interpolation types (a Linear or Hold side keeps its type and stores the
// ease). Returns the number of keys changed.
RK.anim.applyEase = function (comp, o) {
    var touchIn = o.speed !== null || o.inf !== null, touchOut = o.speed !== null || o.outf !== null;
    if (!touchIn && !touchOut) return 0;
    var c = RK.anim.collect(comp), refs = [], restore = {};
    for (var i = 0; i < c.props.length; i++) {
        var e = c.props[i];
        if (e.info.interpolates === false) continue;
        for (var j = 0; j < e.sel.length; j++) {
            var key = e.keys[e.sel[j]];
            var r = { layer: e.layer.id, prop: e.prop.__uid, index: e.sel[j] };
            refs.push(r);
            if ((touchIn && key.inInterp !== "BEZIER") || (touchOut && key.outInterp !== "BEZIER")) {
                var sig = key.inInterp + "/" + key.outInterp;
                if (!restore[sig]) restore[sig] = [];
                restore[sig].push(r);
            }
        }
    }
    if (!refs.length) return 0;
    return RK.undo(RK.NAME + " - Interpolation", function () {
        var p = { };
        if (o.speed !== null) {
            p.inSpeed = o.speed;
            p.outSpeed = o.speed;
        }
        if (o.inf !== null) p.inInfluence = RK.anim.clamp(o.inf, 0.1, 100);
        if (o.outf !== null) p.outInfluence = RK.anim.clamp(o.outf, 0.1, 100);
        RK.anim.cmd(comp, "keys.velocity", p, refs);
        for (var s in restore) {
            if (!restore.hasOwnProperty(s)) continue;
            var t = s.split("/");
            RK.anim.cmd(comp, "keys.interpolation", { "in": t[0].toLowerCase(), out: t[1].toLowerCase() }, restore[s]);
        }
        return refs.length;
    });
};

// "Get": speed and influences of the first selected key of the first selected layer (its first
// selected property that has selected keys), first dimension. null when there is none.
RK.anim.getEase = function (comp) {
    var layers = comp.selectedLayers;
    if (!layers.length) return null;
    var props = layers[0].selectedProperties;
    for (var i = 0; i < props.length; i++) {
        var p = props[i];
        if (!(p instanceof Property) || !p.numKeys) continue;
        var keys = p.__keys();
        for (var k = 0; k < keys.length; k++) {
            if (!keys[k].selected) continue;
            var ie = keys[k].inEase[0] || { speed: 0, influence: 33 }, oe = keys[k].outEase[0] || { speed: 0, influence: 33 };
            return { speed: ie.speed, inf: ie.influence, outf: oe.influence };
        }
    }
    return null;
};

// Spatial interpolation of the selected keys of spatial properties: "linear" (no tangents) or
// "bezier" (smooth tangents, Auto Bezier). Returns the number of keys changed.
RK.anim.spatial = function (comp, mode) {
    var c = RK.anim.collect(comp), refs = [];
    for (var i = 0; i < c.props.length; i++) {
        var e = c.props[i];
        if (!e.info.spatial) continue;
        for (var j = 0; j < e.sel.length; j++) refs.push({ layer: e.layer.id, prop: e.prop.__uid, index: e.sel[j] });
    }
    if (!refs.length) return 0;
    return RK.undo(RK.NAME + " - Spatial Interpolation", function () {
        RK.anim.cmd(comp, "keys.interpolation", { spatial: mode === "linear" ? "linear" : "autoBezier" }, refs);
        return refs.length;
    });
};

// ---------------------------------------------------------------------------- morpher
//
// One slider times several properties: each selected property shows its own keyframed value
// at the time the slider gives, in frames (the frame number the timeline displays, so the
// comp's start frame counts). With "Keyframes", the slider gets a key at each key of those
// properties, valued that key's frame: the animation then replays unchanged until the slider
// is re-timed.

RK.anim.isMorpherParam = function (p) {
    try {
        var g = p.parentProperty;
        return !!(g && g.isEffect && /^Morpher(_\d+)?$/.test(g.name));
    } catch (e) {
        return false;
    }
};

RK.anim.morpherExpr = function (layerName, fxName) {
    return RK.xHead("Morpher") + RK.xFast +
        "var L = rk(function () { return rkLayer(" + RK.q(layerName) + "); });\n" +
        "var m = L == null ? null : rk(function () { return rkVal(L, " + RK.q(RK.fxPath(fxName)) + ", function (L) { return L.effect(" + RK.q(fxName) + ")(1).value; }); });\n" +
        "var fd = rk(function () { return thisComp.frameDuration; });\n" +
        "var st = rk(function () { return thisComp.displayStartTime; });\n" +
        "var v = m == null || fd == null || st == null ? null : rk(function () { return thisProperty.valueAtTime(m * fd - st); });\n" +
        "if (rk_wait) { value; } else { v; }\n";
};

// Add a Morpher to the first selected layer, driving every selected property of the selected
// layers. keys: add the slider's keys. Returns {effect, count, keys}, or null after a message.
RK.anim.morpher = function (comp, keys) {
    var layers = comp.selectedLayers;
    if (!layers.length) {
        RK.msg("No layer is selected. Select a layer or a property.");
        return null;
    }
    var L1 = layers[0], targets = [], seen = {};
    for (var i = 0; i < layers.length; i++) {
        var props = layers[i].selectedProperties;
        for (var j = 0; j < props.length; j++) {
            var p = props[j];
            if (!RK.canExpr(p) || RK.anim.isMorpherParam(p)) continue;
            var id = layers[i].id + ":" + p.__uid;
            if (seen[id]) continue;
            seen[id] = true;
            targets.push(p);
        }
    }
    return RK.undo("Create a morpher", function () {
        var name = RK.uniqueEffectName(L1, "Morpher");
        var fx = RK.addSlider(L1, name, 0);
        RK.fxOff(L1, fx);
        var code = RK.anim.morpherExpr(L1.name, name);
        var times = [], seenT = {}, a, k;
        for (a = 0; a < targets.length; a++) {
            RK.setExpr(targets[a], code);
            if (!keys || !targets[a].numKeys) continue;
            var ks = targets[a].__keys();
            for (k = 0; k < ks.length; k++) {
                var tk = Math.round(ks[k].time * 1e6);
                if (seenT[tk]) continue;
                seenT[tk] = true;
                times.push(ks[k].time);
            }
        }
        times.sort(function (x, y) { return x - y; });
        var fd = comp.frameDuration, st = comp.displayStartTime, slider = fx.property(1);
        for (k = 0; k < times.length; k++) slider.setValueAtTime(times[k], (times[k] + st) / fd);
        return { effect: fx, count: targets.length, keys: times.length };
    });
};

// ---------------------------------------------------------------------------- copy / paste anim
//
// Copy anim records, in this module's memory, every keyframeable property of the selected
// layers' Transform, masks and effects: the static (pre-expression) value at the start time
// and the keys in the copied span with all their interpolation, times relative to the start.
// With selected keys only those are copied, from the earliest one to the end of the work
// area; otherwise the whole work area. Paste anim matches layers by name, index or selection
// order (Settings), properties by name, and puts the keys at the current time. Expressions are
// never copied.

RK.anim.clip = null;

// Transform properties After Effects hides on 2D layers, and the hidden side of a separated
// Position: a copy records their value only, and a paste never targets them.
RK.anim.HIDDEN_2D = { "ADBE Orientation": true, "ADBE Rotate X": true, "ADBE Rotate Y": true, "ADBE Position_2": true };
RK.anim.hiddenProp = function (info, is3D) {
    if (info.hidden || info.separated) return true;
    return !is3D && RK.anim.HIDDEN_2D[info.matchName] === true;
};

RK.anim.keyRec = function (k, t0, info) {
    var r = {
        t: k.time - t0, value: k.value, inInterp: k.inInterp, outInterp: k.outInterp, inEase: k.inEase, outEase: k.outEase,
        continuous: k.continuous, autoBezier: k.autoBezier
    };
    if (info.spatial) r.sp = { "in": k.spatialIn, out: k.spatialOut, continuous: k.spatialContinuous, auto: k.spatialAutoBezier, roving: k.roving };
    return r;
};

// One property's record, or null to leave it out. o: {t0 (times are relative to it), from
// (default t0) and t1 (the keys copied), onlySelected, is3D}
RK.anim.propRec = function (p, o, info) {
    info = info || p.__info();
    var vt = info.valueType;
    if (vt === "NO_VALUE" || vt === "CUSTOM_VALUE" || vt === "MARKER" || !info.canVaryOverTime) return null;
    var keys = [], from = o.from === undefined ? o.t0 : o.from;
    if (info.numKeys && !RK.anim.hiddenProp(info, o.is3D)) {
        var all = p.__keys();
        for (var i = 0; i < all.length; i++) {
            var k = all[i];
            if (k.time < from - 1e-6 || k.time > o.t1 + 1e-6) continue;
            if (o.onlySelected && !k.selected) continue;
            keys.push(RK.anim.keyRec(k, o.t0, info));
        }
    }
    if (o.onlySelected && !keys.length) return null;
    var v;
    try {
        v = p.valueAtTime(isFinite(o.t0) ? o.t0 : 0, true);
    } catch (e) {
        return null;
    }
    return { name: info.name, matchName: info.matchName, vt: vt, value: v, keys: keys, spatial: !!info.spatial };
};

RK.anim.groupRecs = function (g, o, out) {
    for (var i = 1; i <= g.numProperties; i++) {
        var p = g.property(i);
        if (!p) continue;
        if (p instanceof Property) {
            var r = RK.anim.propRec(p, o);
            if (r) out.push(r);
        } else RK.anim.groupRecs(p, o, out);
    }
    return out;
};

RK.anim.layerRec = function (L, o) {
    o.is3D = RK.anim.is3D(L);
    var rec = { name: L.name, index: L.index, transform: [], masks: [], effects: [] };
    var tg = L.property("ADBE Transform Group");
    if (tg) RK.anim.groupRecs(tg, o, rec.transform);
    var lists = [["ADBE Mask Parade", rec.masks], ["ADBE Effect Parade", rec.effects]];
    for (var l = 0; l < lists.length; l++) {
        var g = L.property(lists[l][0]);
        if (!g) continue;
        for (var i = 1; i <= g.numProperties; i++) {
            var m = g.property(i);
            var props = RK.anim.groupRecs(m, o, []);
            if (props.length) lists[l][1].push({ name: m.name, matchName: m.matchName, props: props });
        }
    }
    return rec;
};

RK.anim.countKeys = function (rec) {
    var n = 0, i, j;
    var add = function (list) { for (var k = 0; k < list.length; k++) n += list[k].keys.length; };
    add(rec.transform);
    for (i = 0; i < rec.masks.length; i++) add(rec.masks[i].props);
    for (j = 0; j < rec.effects.length; j++) add(rec.effects[j].props);
    return n;
};

// Copy the selected layers' animation. Returns the clipboard, or null after a message.
RK.anim.copy = function (comp) {
    var layers = comp.selectedLayers;
    if (!layers.length) {
        RK.msg("Please select the layers from which you want to save animation.");
        return null;
    }
    var wa0 = comp.workAreaStart, wa1 = wa0 + comp.workAreaDuration, first = null, i;
    // Selected keys show up in the selected properties: the earliest one starts the copy.
    for (i = 0; i < layers.length; i++) {
        var props = layers[i].selectedProperties;
        for (var j = 0; j < props.length; j++) {
            var p = props[j];
            if (!(p instanceof Property) || !p.numKeys) continue;
            var ks = p.__keys();
            for (var k = 0; k < ks.length; k++) if (ks[k].selected && (first === null || ks[k].time < first)) first = ks[k].time;
        }
    }
    var o = first === null ? { t0: wa0, t1: wa1, onlySelected: false } : { t0: first, t1: wa1, onlySelected: true };
    var clip = { layers: [], t0: o.t0, selectedKeys: first !== null, keys: 0 };
    for (i = 0; i < layers.length; i++) {
        var rec = RK.anim.layerRec(layers[i], o);
        clip.keys += RK.anim.countKeys(rec);
        clip.layers.push(rec);
    }
    RK.anim.clip = clip;
    return clip;
};

// Which copied layer each target layer receives. match: "names" | "indexes" | "selection".
RK.anim.pastePairs = function (comp, clip, match) {
    var out = [], i;
    if (match === "selection") {
        var sel = comp.selectedLayers;
        for (i = 0; i < sel.length && i < clip.layers.length; i++) out.push({ layer: sel[i], n: i });
        return out;
    }
    var info = app.run("comp.info", { comp: comp.id }).layers;
    for (i = 0; i < info.length; i++) {
        for (var k = 0; k < clip.layers.length; k++) {
            var r = clip.layers[k];
            if (match === "indexes" ? r.index === info[i].index : r.name === info[i].name) {
                out.push({ layer: comp.layer(info[i].index), n: k });
                break;
            }
        }
    }
    return out;
};

// Paste the clipboard at the current time. Returns {pasted, notFound, errors}, or null with an
// empty clipboard.
RK.anim.paste = function (comp, match) {
    var clip = RK.anim.clip;
    if (!clip || !clip.layers.length) return null;
    var pairs = RK.anim.pastePairs(comp, clip, match || "names");
    var t = comp.time, errors = [], used = {}, nUsed = 0, i;
    if (pairs.length) {
        RK.undo(RK.NAME + " - Paste Anim", function () {
            for (var a = 0; a < pairs.length; a++) {
                if (pairs[a].layer.locked) continue;
                RK.anim.pasteLayer(pairs[a].layer, clip.layers[pairs[a].n], t, errors);
            }
        });
    }
    for (i = 0; i < pairs.length; i++) {
        if (used[pairs[i].n]) continue;
        used[pairs[i].n] = true;
        nUsed++;
    }
    return { pasted: pairs.length, notFound: clip.layers.length - nUsed, errors: errors };
};

// The keyframeable properties of a group that aren't hidden, in order, recursively.
RK.anim.leafProps = function (g, is3D, out) {
    for (var i = 1; i <= g.numProperties; i++) {
        var p = g.property(i);
        if (!p) continue;
        if (p instanceof Property) {
            var info = p.__info();
            if (info.canVaryOverTime && !RK.anim.hiddenProp(info, is3D)) out.push({ prop: p, info: info });
        } else RK.anim.leafProps(p, is3D, out);
    }
    return out;
};

RK.anim.childNamed = function (g, name, matchName) {
    for (var i = 1; i <= g.numProperties; i++) {
        var c = g.property(i);
        if (c && c.name === name && (!matchName || c.matchName === matchName)) return c;
    }
    return null;
};

RK.anim.pasteLayer = function (L, rec, t, errors) {
    var is3D = RK.anim.is3D(L);
    var apply = function (g, recs) {
        if (!g || !recs.length) return;
        var list = RK.anim.leafProps(g, is3D, []);
        for (var i = 0; i < recs.length; i++) {
            for (var j = 0; j < list.length; j++) {
                if (list[j].info.name !== recs[i].name) continue;
                RK.anim.pasteProp(L, list[j].prop, list[j].info, recs[i], t, errors);
                break;
            }
        }
    };
    apply(L.property("ADBE Transform Group"), rec.transform);
    var groups = [["ADBE Mask Parade", rec.masks, false], ["ADBE Effect Parade", rec.effects, true]];
    for (var a = 0; a < groups.length; a++) {
        if (!groups[a][1].length) continue;
        var parade = L.property(groups[a][0]);
        if (!parade) continue;
        for (var b = 0; b < groups[a][1].length; b++) {
            var r = groups[a][1][b];
            apply(RK.anim.childNamed(parade, r.name, groups[a][2] ? r.matchName : null), r.props);
        }
    }
};

// A value shaped for the target: 3-value targets take a 2-value source padded with 0 and a
// 1-value one as (v, 0, 0); 2-value targets the first two values; 1-value targets the first.
RK.anim.fit = function (v, info) {
    var t = info.valueType;
    var d = t === "OneD" ? 1 : t === "TwoD" || t === "TwoD_SPATIAL" ? 2 : t === "ThreeD" || t === "ThreeD_SPATIAL" ? info.dims || 3 : 0;
    if (!d || v === null || v === undefined || typeof v === "object" && !(v instanceof Array)) return v;
    var a = v instanceof Array ? v : [v];
    if (d === 1) return a[0];
    var out = [];
    for (var i = 0; i < d; i++) out.push(i < a.length ? a[i] : 0);
    return out;
};

// Paste one property record at time t: its keys, or (none) its static value (a keyframed
// target keeps its keys).
RK.anim.pasteProp = function (L, q, info, r, t, errors) {
    if (!r.keys.length) {
        try {
            q.setValue(RK.anim.fit(r.value, info));
        } catch (e) {}
        return;
    }
    var placed = [];
    for (var i = 0; i < r.keys.length; i++) {
        try {
            q.setValueAtTime(t + r.keys[i].t, RK.anim.fit(r.keys[i].value, info));
            placed.push(r.keys[i]);
        } catch (e) {}
    }
    if (placed.length) RK.anim.keyData(L, q, info, placed, t, errors);
};

RK.anim.isZero = function (v) {
    if (!v) return true;
    for (var i = 0; i < v.length; i++) if (Math.abs(v[i]) > 1e-9) return false;
    return true;
};

// Interpolation of pasted keys (they were just set with their values): spatial modes, tangents
// and roving, then eases, types and flags. Keys sharing the same settings go in one command.
RK.anim.keyData = function (L, q, info, placed, t, errors) {
    var comp = L.containingComp, keys = q.__keys(), half = comp.frameDuration / 2;
    var groups = [], byId = {};
    var group = function (order, id, params, ref) {
        var sig = order + id + JSON.stringify(params);
        if (!byId[sig]) {
            byId[sig] = { order: order, id: id, params: params, refs: [] };
            groups.push(byId[sig]);
        }
        byId[sig].refs.push(ref);
    };
    var nums = function (list, k) {
        var o = [];
        for (var i = 0; i < list.length; i++) o.push(list[i][k]);
        return o;
    };
    for (var i = 0; i < placed.length; i++) {
        var kr = placed[i], at = t + kr.t, idx = -1, best = half + 1e-6;
        for (var j = 0; j < keys.length; j++) {
            var d = Math.abs(keys[j].time - at);
            if (d < best) {
                best = d;
                idx = j;
            }
        }
        if (idx < 0) continue;
        var ref = { layer: L.id, prop: q.__uid, index: idx };
        if (kr.sp && info.spatial) {
            var sp = kr.sp, mode = null;
            if (sp.auto) mode = "autoBezier";
            else if (!sp.continuous && RK.anim.isZero(sp["in"]) && RK.anim.isZero(sp.out)) mode = "linear";
            else {
                try {
                    q.setSpatialTangentsAtKey(idx + 1, sp["in"], sp.out);
                } catch (e) {
                    errors.push(RK.err(e));
                }
                if (sp.continuous) mode = "continuousBezier";
            }
            var sparams = { roving: !!sp.roving };
            if (mode) sparams.spatial = mode;
            group(1, "keys.interpolation", sparams, ref);
        }
        var v = {};
        if (kr.inInterp === "BEZIER" && kr.inEase.length) {
            v.inSpeed = nums(kr.inEase, "speed");
            v.inInfluence = nums(kr.inEase, "influence");
        }
        if (kr.outInterp === "BEZIER" && kr.outEase.length) {
            v.outSpeed = nums(kr.outEase, "speed");
            v.outInfluence = nums(kr.outEase, "influence");
        }
        if (v.inSpeed || v.outSpeed) {
            v.continuous = !!kr.continuous;
            group(2, "keys.velocity", v, ref);
        }
        if (info.interpolates !== false) {
            group(3, "keys.interpolation", { "in": String(kr.inInterp).toLowerCase(), out: String(kr.outInterp).toLowerCase(), continuous: !!kr.continuous, autoBezier: !!kr.autoBezier }, ref);
            if (kr.autoBezier && kr.continuous) group(4, "keys.interpolation", { continuous: true }, ref);
        }
    }
    groups.sort(function (a, b) { return a.order - b.order; });
    for (var g = 0; g < groups.length; g++) {
        try {
            RK.anim.cmd(comp, groups[g].id, groups[g].params, groups[g].refs);
        } catch (e) {
            errors.push(RK.err(e));
        }
    }
};

// ---------------------------------------------------------------------------- cel animation
//
// Hand-drawn animation with the Paint effect: one cel per solid (or per Paint effect on one
// layer). Each stroke's duration bar is its "in" / "out" (layer seconds; out 1e9 = forever).
// Apply holds every new stroke for the exposure (E frames) and, with the onion skin on,
// stretches the bar D frames before and/or after and fades the stroke's Opacity there. The
// state is kept in the stroke's name: " #RigKit" once its exposure is set, " #In" / " #Out"
// while the onion skin fades it in / out.

RK.anim.PAINT = "ec.paint.paint";
RK.anim.CEL_TAG = " #" + RK.NAME.replace(/\s+/g, "");
RK.anim.STROKES = { brush: true, clone: true, eraser: true };

// The cel's state, kept while the panel is open (the page is rebuilt each time it opens).
RK.anim.celState = { single: false, onion: true, dur: 5, inOn: false, inOp: 50, outOn: true, outOp: 50, expo: 2 };

// The layer the cel tools work on: the first selected layer, else the topmost layer whose name
// contains "Cel".
RK.anim.celTarget = function (comp) {
    var sel = comp.selectedLayers;
    if (sel.length) return sel[0];
    var info = app.run("comp.info", { comp: comp.id }).layers;
    for (var i = 0; i < info.length; i++) if (String(info[i].name).indexOf("Cel") >= 0) return comp.layer(info[i].index);
    return null;
};

// New cel: a new black comp-sized solid "Cel N" (N after the highest "Cel <n>" in the comp),
// or with `single` a new Paint effect on the target layer. The first Paint effect of a layer
// paints on transparency. Returns {layer, paint}.
RK.anim.newCel = function (comp, single) {
    var target = RK.anim.celTarget(comp);
    return RK.undo(RK.NAME + " - New celluloid", function () {
        var layer = single && target ? target : null;
        if (!layer) {
            var info = app.run("comp.info", { comp: comp.id }).layers, n = 0;
            for (var i = 0; i < info.length; i++) {
                var m = /cel\s*(\d+)/i.exec(info[i].name);
                if (m) n = Math.max(n, parseInt(m[1], 10));
            }
            layer = comp.layers.addSolid([0, 0, 0], "Cel " + (n + 1), comp.width, comp.height, comp.pixelAspect, comp.duration);
        }
        var fx = layer.property("ADBE Effect Parade"), first = true;
        for (var k = 1; k <= fx.numProperties; k++) if (fx.property(k).matchName === RK.anim.PAINT) first = false;
        var paint = fx.addProperty(RK.anim.PAINT);
        if (first) paint.property(1).setValue(1);
        return { layer: layer, paint: paint };
    });
};

// The strokes of the layer's Paint effects, from one property tree query:
// [{uid, name, effect (index among the Paint effects), inUid, outUid, b, e (comp seconds),
//   opUid, keys: [{t (comp seconds), v}]}]
RK.anim.strokes = function (comp, layer) {
    var tree = app.run("layer.tree", { comp: comp.id, layer: layer.id, depth: 6 });
    var start = tree.start || 0, k = (tree.stretch || 100) / 100;
    var toComp = function (lt) { return start + lt * k; };
    var out = [], kids = tree.properties.children, fxs = null, i, j, n = 0;
    for (i = 0; i < kids.length; i++) if (kids[i].match === "effects") fxs = kids[i].children;
    if (!fxs) return { list: out, start: start, k: k };
    for (i = 0; i < fxs.length; i++) {
        if (fxs[i].effect !== RK.anim.PAINT) continue;
        n++;
        var items = fxs[i].children || [];
        for (j = 0; j < items.length; j++) {
            var g = items[j];
            if (!RK.anim.STROKES[g.match] || !g.children) continue;
            var s = { uid: g.uid, name: g.name, effect: n, opUid: null, keys: [] };
            for (var c = 0; c < g.children.length; c++) {
                var p = g.children[c];
                if (p.match === "in") {
                    s.inUid = p.uid;
                    s.b = toComp(p.value);
                } else if (p.match === "out") {
                    s.outUid = p.uid;
                    s.e = p.value >= 1e8 ? p.value : toComp(p.value);
                } else if (p.match === "stroke_options" && p.children) {
                    for (var o = 0; o < p.children.length; o++) {
                        var q = p.children[o];
                        if (q.match !== "opacity") continue;
                        s.opUid = q.uid;
                        var ks = q.keys || [];
                        for (var x = 0; x < ks.length; x++) s.keys.push({ t: toComp(ks[x].time), v: ks[x].value });
                    }
                }
            }
            if (s.inUid !== undefined && s.outUid !== undefined) out.push(s);
        }
    }
    return { list: out, start: start, k: k };
};

// The stroke's own span (comp seconds) from the onion-skin keys and its bar, whatever D, E and
// the opacities were: the "out" key is always the one before the last, the "in" key the last
// with only a fade in; with both fades the bar starts D frames before "in" and ends D frames
// after "out".
RK.anim.celSpan = function (s, tagIn, tagOut) {
    var n = s.keys.length, inT = s.b, outT = s.e;
    if (tagOut && n >= 2) outT = s.keys[n - 2].t;
    if (tagIn && tagOut && n >= 2) inT = s.b + (s.e - outT);
    else if (tagIn && n >= 1) inT = s.keys[n - 1].t;
    return [inT, outT];
};

// Apply the exposure and the onion skin to the target layer's strokes.
// o: {active, dur (D frames), expo (E frames), inOp (I %, 0 = no fade in), outOp (O %)}.
// Returns the number of strokes, or null without a target layer.
RK.anim.onion = function (comp, o) {
    var layer = RK.anim.celTarget(comp);
    if (!layer) return null;
    return RK.undo(RK.NAME + " - Update Onion Skin", function () {
        return RK.withUnlocked(layer, function () { return RK.anim.onionLayer(comp, layer, o); });
    });
};

RK.anim.onionLayer = function (comp, layer, o) {
    var fd = comp.frameDuration, S = RK.anim.strokes(comp, layer), tag = RK.anim.CEL_TAG;
    var D = Math.max(1, Math.round(o.dur || 1)) * fd, E = Math.max(1, Math.round(o.expo || 1)) * fd;
    var I = o.inOp > 0 ? Math.min(100, o.inOp) : 0, O = o.outOp > 0 ? Math.min(100, o.outOp) : 0;
    var toLayer = function (ct) { return (ct - S.start) / S.k; };
    var run = function (id, p) {
        p.comp = comp.id;
        p.layer = layer.id;
        return app.run(id, p);
    };
    var setSpan = function (s, a, b) {
        run("prop.set", { prop: s.inUid, value: toLayer(a) });
        run("prop.set", { prop: s.outUid, value: b >= 1e8 ? b : toLayer(b) });
    };
    var clearKeys = function (s) {
        var refs = [];
        for (var i = 0; i < s.keys.length; i++) refs.push({ layer: layer.id, prop: s.opUid, index: i });
        RK.anim.cmd(comp, "keys.delete", {}, refs);
        run("prop.set", { prop: s.opUid, value: 100 });
    };
    var key = function (s, t, v) { run("prop.addKey", { prop: s.opUid, time: toLayer(t), value: v }); };
    for (var i = 0; i < S.list.length; i++) {
        var s = S.list[i], name = s.name;
        // 1. exposure, once per stroke (later changes to its bar are the user's)
        if (name.indexOf(tag) < 0) {
            s.e = s.b + E;
            run("prop.set", { prop: s.outUid, value: toLayer(s.e) });
            name += tag;
        }
        var tagIn = name.indexOf(" #In") >= 0, tagOut = name.indexOf(" #Out") >= 0;
        // Opacity keys on a stroke without fade tags are the user's: leave them alone.
        var ours = s.keys.length === 0 || tagIn || tagOut;
        var span = RK.anim.celSpan(s, tagIn, tagOut);
        // (a stroke made to last forever has no end to fade out from)
        var Os = span[1] >= 1e8 ? 0 : O;
        if (ours && s.opUid !== null) {
            if (s.keys.length) clearKeys(s);
            name = name.replace(/ #Out/g, "").replace(/ #In/g, "");
            if (o.active && (I > 0 || Os > 0)) {
                var inT = span[0], outT = span[1];
                setSpan(s, I > 0 ? inT - D : inT, Os > 0 ? outT + D : outT);
                if (Os > 0) {
                    key(s, outT - fd, 100);
                    key(s, outT, Os);
                    key(s, outT + D, 0);
                    name += " #Out";
                }
                if (I > 0) {
                    key(s, inT, 100);
                    key(s, inT - D, 0);
                    key(s, inT - fd, I);
                    name += " #In";
                }
            } else if (tagIn || tagOut) setSpan(s, span[0], span[1]);
        }
        if (name !== s.name) run("prop.renameGroup", { prop: s.uid, name: name });
    }
    var sel = layer.selectedProperties;
    for (var k = 0; k < sel.length; k++) {
        try {
            sel[k].selected = false;
        } catch (e) {}
    }
    return S.list.length;
};

// The onion skin the target layer's strokes show (the last Paint effect's first stroke):
// {active, dur, inOp, outOp, expo}. The defaults without a target or a stroke.
RK.anim.onionGet = function (comp) {
    var d = { active: false, dur: 5, inOp: 0, outOp: 50, expo: 2 };
    var layer = RK.anim.celTarget(comp);
    if (!layer) return d;
    var S = RK.anim.strokes(comp, layer).list, s = null, eff = -1;
    for (var i = 0; i < S.length; i++) {
        if (S[i].effect !== eff) {
            eff = S[i].effect;
            s = S[i];
        }
    }
    if (!s) return d;
    var tagIn = s.name.indexOf(" #In") >= 0, tagOut = s.name.indexOf(" #Out") >= 0;
    if (!(tagIn || tagOut) || !s.keys.length) return { active: false, dur: d.dur, inOp: d.inOp, outOp: d.outOp, expo: d.expo };
    var fd = comp.frameDuration, span = RK.anim.celSpan(s, tagIn, tagOut), inT = span[0], outT = span[1];
    var valueAt = function (t) {
        for (var k = 0; k < s.keys.length; k++) if (Math.abs(s.keys[k].t - t) < fd / 2) return s.keys[k].v;
        return 0;
    };
    var r = { active: true, inOp: 0, outOp: 0, expo: Math.round((outT - inT) / fd) };
    if (tagOut) {
        r.outOp = valueAt(outT);
        r.dur = Math.round((s.e - outT) / fd);
    }
    if (tagIn) {
        r.inOp = valueAt(inT - fd);
        r.dur = Math.round((inT - s.b) / fd);
    }
    return r;
};

// ---------------------------------------------------------------------------- import rig
//
// A rig comp is instantiated in the active comp as a renamed deep copy: the comp and every comp
// nested in it are duplicated with the prefix "<instance>_" into a new folder, and expressions
// naming the copied comps are pointed at the copies. EffectCraft can't change a layer's source
// from a script, so a layer of a copied comp still shows the original nested comp. To show the
// copy instead, the layer is pre-composed with "Leave all attributes": it stays the same layer
// (transform, keys, effects, masks, expressions, parent, children, switches and timing all
// untouched) and its new source, a wrapper comp of the nested comp's size, duration and frame
// rate, then holds the nested comp's copy. One wrapper comp per such layer.
//
// Import then adds the copy to the active comp, gives each controller of the copy a zero, makes
// a zero and a controller per controller in the active comp (same look), moves the original
// controllers' animation, values and expressions there and links the originals to them, and
// adds a MASTER controller (with Flip) that carries the rig layer.

RK.anim.comps = function () {
    var out = [], n = app.project.numItems;
    for (var i = 1; i <= n; i++) {
        var it = app.project.item(i);
        if (it instanceof CompItem) out.push({ comp: it, label: i + " " + it.name });
    }
    return out;
};

RK.anim.itemNames = function () {
    var names = {}, n = app.project.numItems;
    for (var i = 1; i <= n; i++) names[app.project.item(i).name] = true;
    return names;
};

RK.anim.uniqueItemName = function (names, base) {
    var n = base, i = 2;
    while (names[n]) n = base + " " + i++;
    names[n] = true;
    return n;
};

// The comp and every comp nested in it, once each.
RK.anim.nestedComps = function (R) {
    var out = [], seen = {};
    var walk = function (C) {
        if (seen[C.id]) return;
        seen[C.id] = true;
        out.push(C);
        for (var i = 1; i <= C.numLayers; i++) {
            var src = null;
            try {
                src = C.layer(i).source;
            } catch (e) {}
            if (src instanceof CompItem) walk(src);
        }
    };
    walk(R);
    return out;
};

// The names a deep copy would take that are already used in the project.
RK.anim.nameClashes = function (R, prefix) {
    var names = RK.anim.itemNames(), list = RK.anim.nestedComps(R), out = [];
    for (var i = 0; i < list.length; i++) if (names[prefix + list[i].name]) out.push(prefix + list[i].name);
    return out;
};

// Text replacements in the expressions of a comp's layers (only the layers in `only`, by id, if
// given). subs: [[old, new]].
RK.anim.replaceInExprs = function (C, subs, only) {
    if (!subs.length) return 0;
    var info = app.run("comp.info", { comp: C.id }).layers, n = 0;
    var walk = function (node, hits) {
        if (node.expression) {
            var s = node.expression;
            for (var k = 0; k < subs.length; k++) s = s.split(subs[k][0]).join(subs[k][1]);
            if (s !== node.expression) hits.push([node.uid, s]);
        }
        var kids = node.children || [];
        for (var j = 0; j < kids.length; j++) walk(kids[j], hits);
        return hits;
    };
    for (var i = 0; i < info.length; i++) {
        if (only && !only[info[i].id]) continue;
        var tree = app.run("layer.tree", { comp: C.id, layer: info[i].id, depth: 40 });
        var hits = walk(tree.properties, []);
        if (!hits.length) continue;
        var set = function () {
            for (var h = 0; h < hits.length; h++) app.run("prop.setExpression", { comp: C.id, layer: info[i].id, prop: hits[h][0], expression: hits[h][1] });
        };
        RK.withUnlocked(C.layer(info[i].index), set);
        n += hits.length;
    }
    return n;
};

// "name(" + quoted old name -> the new name, in both quote styles.
RK.anim.refSubs = function (fn, pairs) {
    var sq = function (s) { return "'" + String(s).replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "'"; };
    var out = [];
    for (var i = 0; i < pairs.length; i++) {
        if (pairs[i][0] === pairs[i][1]) continue;
        out.push([fn + "(" + RK.q(pairs[i][0]), fn + "(" + RK.q(pairs[i][1])]);
        out.push([fn + "(" + sq(pairs[i][0]), fn + "(" + sq(pairs[i][1])]);
    }
    return out;
};

// Make layer L (in comp C) show `target` instead of the nested comp it shows (see above).
RK.anim.swapSource = function (C, L, target, folder, names, wrappers) {
    var name = L.name, locked = L.locked, collapse = false;
    try {
        collapse = !!L.collapseTransformation;
    } catch (e) {}
    if (locked) L.locked = false;
    var wname = RK.anim.uniqueItemName(names, target.name + " - " + name);
    app.run("layer.precompose", { comp: C.id, layers: [L.id], mode: "leave", name: wname });
    var W = L.source;
    var inner = W.layer(1);
    var nl = W.layers.add(target);
    if (collapse) {
        try {
            nl.collapseTransformation = true;
        } catch (e) {}
    }
    inner.remove();
    try {
        if (W.frameRate !== target.frameRate) W.frameRate = target.frameRate;
        if (W.pixelAspect !== target.pixelAspect) W.pixelAspect = target.pixelAspect;
    } catch (e) {}
    L.name = name;
    if (locked) L.locked = true;
    wrappers.push(W);
    return W;
};

// Deep-duplicate the rig comp R with `prefix`. Returns {comp, folder, comps, pairs, wrappers}.
// (Each command in an undo group counts against EffectCraft's Levels of Undo while the group
// is open: the copies all move to their folder in one command.)
RK.anim.deepDup = function (R, prefix) {
    var names = RK.anim.itemNames();
    var folder = app.project.items.addFolder(prefix + R.name);
    var pf = R.parentFolder;
    if (pf && pf.id !== 0 && pf.parentFolder && pf.parentFolder.id !== 0) folder.parentFolder = pf.parentFolder;
    var map = {}, pairs = [], wrappers = [], comps = [];
    var dup = function (C0) {
        var copy = C0.duplicate();
        copy.name = prefix + C0.name;
        names[copy.name] = true;
        map[C0.id] = copy;
        comps.push(copy);
        pairs.push([C0.name, copy.name]);
        var shows = [], i;
        for (i = 1; i <= copy.numLayers; i++) {
            var L = copy.layer(i), src = null;
            try {
                src = L.source;
            } catch (e) {}
            if (src instanceof CompItem) shows.push([L, src]);
        }
        for (i = 0; i < shows.length; i++) {
            var target = map[shows[i][1].id] || dup(shows[i][1]);
            RK.anim.swapSource(copy, shows[i][0], target, folder, names, wrappers);
        }
        return copy;
    };
    var top = dup(R);
    var ids = [], c;
    for (c = 0; c < comps.length; c++) ids.push(comps[c].id);
    for (c = 0; c < wrappers.length; c++) ids.push(wrappers[c].id);
    app.run("project.move", { items: ids, folder: folder.id });
    var subs = RK.anim.refSubs("comp", pairs);
    for (c = 0; c < comps.length; c++) RK.anim.replaceInExprs(comps[c], subs);
    return { comp: top, folder: folder, comps: comps, pairs: pairs, wrappers: wrappers };
};

// "Duplicate": a renamed deep copy of R. Returns the deep copy, or null after a message.
RK.anim.duplicateRig = function (R, inst) {
    if (!RK.anim.checkInstance(R, inst)) return null;
    return RK.undo("Duplicate rig: " + inst, function () {
        return RK.anim.deepDup(R, inst + "_");
    });
};

RK.anim.checkInstance = function (R, inst) {
    if (!R) {
        RK.msg("Choose the rigged composition to import.");
        return false;
    }
    if (RK.trim(inst) === "") {
        RK.msg("You must specify a unique name for this instance.");
        return false;
    }
    var clash = RK.anim.nameClashes(R, inst + "_");
    if (clash.length) {
        RK.msg("\"" + clash[0] + "\" already exists: the name of this instance must be unique.");
        return false;
    }
    return true;
};

// The expression that makes a property of the rig (original controller) read the same
// property of its new controller in comp `compName`.
RK.anim.linkExpr = function (compName, layerName, path, api) {
    var ln = RK.q(layerName);
    return RK.xHead("link") + RK.xFast +
        "var C = rk(function () { return comp(" + RK.q(compName) + "); });\n" +
        "var L = C == null ? null : rk(function () {\n" +
        "    if (!rkF) return C.layer(" + ln + ");\n" +
        "    var id = __h(__LAYER, C.__c, " + ln + ");\n" +
        "    if (id === null) throw new Error(\"Layer \" + " + ln + " + \" does not exist\");\n" +
        "    return id;\n" +
        "});\n" +
        "var v = L == null ? null : rk(function () { return rkF && " + (path ? "true" : "false") + " ? __h(__VALUE, C.__c, L, " + RK.q(path) + ", time, false) : " + api + ".value; });\n" +
        "if (rk_wait) { value; } else { v; }\n";
};

// The parameters of an effect and of its copy, by position (groups too).
RK.anim.fxPairs = function (g, h, fxName, path, api, out) {
    for (var i = 1; i <= g.numProperties && i <= h.numProperties; i++) {
        var p = g.property(i), q = h.property(i);
        if (!p || !q) continue;
        if (p instanceof Property) {
            if (!(q instanceof Property)) continue;
            var info = p.__info();
            if (!info.canVaryOverTime || info.hidden) continue;
            out.push({ p: p, info: info, q: q, path: path ? path + "/#" + i : "", api: api + "(" + i + ")" });
        } else RK.anim.fxPairs(p, q, fxName, path ? path + "/#" + i : "", api + "(" + i + ")", out);
    }
    return out;
};

// Give the new controller `neu` (in comp C) the original controller's animation, values and
// expressions, then make the original read the new one (its Transform, and the effects paired
// in fxPairs [[original effect, new effect]]).
RK.anim.linkLayer = function (orig, neu, C, fxPairs, errors) {
    var is3D = RK.anim.is3D(orig), pairs = [], i;
    var ot = orig.property("ADBE Transform Group"), nt = neu.property("ADBE Transform Group");
    for (i = 1; i <= ot.numProperties; i++) {
        var p = ot.property(i);
        if (!(p instanceof Property)) continue;
        var info = p.__info();
        if (!info.canVaryOverTime || RK.anim.hiddenProp(info, is3D)) continue;
        var q = nt.property(info.matchName);
        if (!q) continue;
        pairs.push({ p: p, info: info, q: q, path: "transform/" + info.matchId, api: "L.transform(" + RK.q(info.name) + ")" });
    }
    for (i = 0; i < fxPairs.length; i++) {
        var nm = fxPairs[i][1].name;
        RK.anim.fxPairs(fxPairs[i][0], fxPairs[i][1], nm, /[\/.]/.test(nm) ? "" : "effects/" + nm, "L.effect(" + RK.q(nm) + ")", pairs);
    }
    var o = { t0: 0, from: -Infinity, t1: Infinity, onlySelected: false, is3D: true };
    for (i = 0; i < pairs.length; i++) {
        var pr = pairs[i];
        var rec = RK.anim.propRec(pr.p, o, pr.info);
        if (rec) RK.anim.pasteProp(neu, pr.q, pr.q.__info(), rec, 0, errors);
        if (pr.info.expression) {
            try {
                pr.q.expression = pr.info.expression;
            } catch (e) {}
        }
        try {
            RK.setExpr(pr.p, RK.anim.linkExpr(C.name, neu.name, pr.path, pr.api));
        } catch (e) {
            errors.push(pr.info.name + ": " + RK.err(e));
        }
    }
    return pairs.length;
};

// "Import": the rig comp R as instance `inst` in comp C. Returns {rig, master, controllers,
// errors} or null after a message.
RK.anim.importRig = function (C, R, inst) {
    if (!RK.anim.checkInstance(R, inst)) return null;
    var errors = [];
    return RK.undo("Import rig: " + inst, function () {
        var d = RK.anim.deepDup(R, inst + "_");
        var R2 = d.comp, i, r;
        var rig = C.layers.add(R2);
        try {
            R2.time = C.time - rig.startTime;
        } catch (e) {}
        // the copy's controllers (its own layers only) and their zeros
        var recs = [];
        for (i = 1; i <= R2.numLayers; i++) {
            var L = R2.layer(i);
            if (RK.ctrl.isController(L)) recs.push({ ctrl: L, name: L.name });
        }
        for (i = 0; i < recs.length; i++) {
            r = recs[i];
            var p = r.ctrl.parent;
            r.zero = p && String(p.name).indexOf("Zero") === 0 ? p : RK.zero(r.ctrl);
            r.zeroName = r.zero.name;
            r.zeroParent = r.zero.parent ? r.zero.parent.name : null;
            r.w = RK.world(r.zero);
            r.anchor = r.zero.transform.anchorPoint.value;
            r.info = RK.ctrl.read(r.ctrl);
        }
        // a zero and a controller in C for each, from the last to the first (so they stack in
        // the same order); each zero sits where the rig's zero is, with its rotation and scale
        for (i = recs.length - 1; i >= 0; i--) {
            r = recs[i];
            var z = C.layers.addNull();
            z.parent = rig;
            z.name = "Zero_" + inst + "_" + r.name;
            z.transform.anchorPoint.setValue([r.anchor[0], r.anchor[1], 0]);
            z.transform.position.setValue([r.w.pos[0], r.w.pos[1], 0]);
            RK.rotProp(z).setValue(r.w.rot);
            z.transform.scale.setValue([r.w.sx * 100, r.w.sy * 100, 100]);
            RK.addTag(z, RK.ZERO_TAG);
            RK.hide(z, { bottom: true, guide: true, shy: true, off: true });
            var c = RK.ctrl.create(C, z, { flags: r.info.flags, type: r.info.kind, color: r.info.color, size: r.info.size, name: inst + "_" + r.name, lock: false });
            c.parent = z;
            if (r.ctrl.transform.position.dimensionsSeparated) c.transform.position.dimensionsSeparated = true;
            var ofx = RK.effects(r.ctrl), nfx = RK.effects(c), fxPairs = [];
            for (var k = 1; ofx && k <= ofx.numProperties; k++) {
                var e = ofx.property(k), ne = null;
                try {
                    ne = nfx.addProperty(e.matchName);
                    ne.name = e.name;
                    if (!e.enabled) ne.enabled = false;
                } catch (err) {
                    if (!ne) continue;
                }
                fxPairs.push([e, ne]);
            }
            (function (orig, neu, pairs) {
                RK.withUnlocked(orig, function () { RK.anim.linkLayer(orig, neu, C, pairs, errors); });
            })(r.ctrl, c, fxPairs);
            r.newZero = z;
            r.newCtrl = c;
        }
        // the new controllers' expressions name the new layers
        var names = [], ids = {};
        for (i = 0; i < recs.length; i++) {
            names.push([recs[i].name, recs[i].newCtrl.name], [recs[i].zeroName, recs[i].newZero.name]);
            ids[recs[i].newCtrl.id] = true;
        }
        RK.anim.replaceInExprs(C, RK.anim.refSubs("layer", names), ids);
        // zeros follow the new controllers their rig zeros followed
        for (i = 0; i < recs.length; i++) {
            r = recs[i];
            if (r.zeroParent) {
                var np = C.layer(inst + "_" + r.zeroParent);
                if (np) r.newZero.parent = np;
            }
            r.newZero.locked = true;
        }
        // the master controller carries the rig layer, with a Flip
        var master = RK.ctrl.create(C, rig, { flags: { rotation: true, x: true, y: true, scale: true }, lock: false, name: "MASTER_" + inst });
        var mp = master.transform.position.value;
        master.transform.position.setValue([mp[0], C.height - 20, 0]);
        var flip = RK.addCheckbox(master, "Flip", false);
        RK.fxOff(master, flip);
        // (value has 2 components on a 2D layer, 3 on a 3D one)
        RK.setExpr(master.transform.scale, RK.xHead("Flip") +
            "var f = rk(function () { return effect(\"Flip\")(1).value; });\n" +
            "if (rk_wait) { value; } else if (f) { var s = []; for (var i = 0; i < value.length; i++) s.push(i === 0 ? -value[i] : value[i]); s; } else { value; }\n");
        rig.parent = master;
        rig.locked = true;
        RK.selectOnly(C, [master]);
        return { rig: rig, master: master, controllers: recs.length, copy: d, errors: errors };
    });
};

// ---------------------------------------------------------------------------- tools

RK.tool("copyanim", {
    label: "Copy anim", icon: RK.anim.I.copyAnim,
    tip: "Copy the animation of the selected layers (their selected keys if there are any, otherwise the whole work area)",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var clip = RK.anim.copy(comp);
        if (clip) RK.msg("Copied the animation of " + clip.layers.length + (clip.layers.length === 1 ? " layer" : " layers") + " (" + clip.keys + " keyframes).");
    }
});

RK.tool("pasteanim", {
    label: "Paste anim", icon: RK.anim.I.pasteAnim,
    tip: "Paste the copied animation at the current time, on the layers with the same names (Settings: names, indexes or selection order)",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var r = RK.anim.paste(comp, RK.pref("pasteMatch", "names"));
        if (!r) return;
        var copied = RK.anim.clip.layers.length, s = "";
        if (r.pasted !== copied) {
            s = "Pasted animation on " + r.pasted + (r.pasted === 1 ? " layer." : " layers.");
            if (r.notFound > 0) s += "\n\n" + r.notFound + (r.notFound === 1 ? " layer" : " layers") + " not found.";
        }
        if (r.errors.length) s += (s ? "\n\n" : "") + "Some keyframes could not take all their settings: " + r.errors[0];
        if (s) RK.msg(s);
    }
});

RK.tool("celanim", {
    label: "Cel animation", icon: RK.anim.I.cel,
    tip: "Cel animation tools: paint frame by frame with the Paint effect, with exposure and onion skin",
    page: function (body) { RK.anim.celPage(body); }
});

RK.tool("importrig", {
    label: "Import rig in comp", icon: RK.anim.I.importRig,
    tip: "Import a rigged composition in the active comp as a new instance, its controllers moved into this comp",
    page: function (body) { RK.anim.importPage(body); }
});

// ---------------------------------------------------------------------------- the tab

RK.anim.ui = {};

RK.anim.interactive = function () {
    return !!RK.pref("interactiveUpdate", false) && !RK.anim.ui.buttons;
};

RK.anim.resetPresets = function () {
    var U = RK.anim.ui;
    if (U.presets) U.presets.selection = 0;
};

RK.anim.fillPresets = function (list) {
    var d = RK.anim.ui.presets;
    d.removeAll();
    d.add("item", "Presets");
    for (var i = 0; i < list.length; i++) d.add("item", list[i]);
    d.selection = 0;
};

// Influence rows: the slider or the quick buttons, and the field.
RK.anim.setInfluence = function (which, v) {
    var U = RK.anim.ui;
    U[which + "Field"].text = RK.num(v);
    if (U[which + "Slider"]) U[which + "Slider"].value = RK.anim.clamp(v, 1, 100);
};

RK.anim.setSpeed = function (v) {
    var U = RK.anim.ui;
    U.speedField.text = RK.num(v);
    U.speedSlider.value = RK.anim.clamp(v, -1000, 1000);
};

// Enable each row's slider (or buttons) and field from its toggle; the In toggle is fixed while
// the influences are locked; the Out row only shows while they aren't.
RK.anim.syncRows = function () {
    var U = RK.anim.ui, lock = U.lock.getValue();
    var setRow = function (which, on) {
        U[which + "Field"].enabled = on;
        if (U[which + "Slider"]) U[which + "Slider"].enabled = on;
        var b = U[which + "Buttons"] || [];
        for (var i = 0; i < b.length; i++) b[i].enabled = on;
    };
    if (lock) {
        U.inOn.setValue(true);
        U.outOn.setValue(true);
    }
    U.inOn.enabled = !lock;
    setRow("speed", U.speedOn.getValue());
    setRow("in", U.inOn.getValue());
    setRow("out", U.outOn.getValue());
    if (U.outRow.visible === lock) {
        U.outRow.visible = !lock;
        RK.ui.relayout();
    }
};

RK.anim.setLock = function (on) {
    var U = RK.anim.ui;
    U.lock.setValue(on);
    if (on) RK.anim.setInfluence("out", RK.anim.influence(U.inField.text, 33));
    RK.anim.syncRows();
};

// The fields as the key type buttons read them: whole numbers, 33 when empty or not a number.
RK.anim.typeInfluences = function () {
    var U = RK.anim.ui, inf = RK.anim.influence(U.inField.text, 33);
    return [inf, U.lock.getValue() ? inf : RK.anim.influence(U.outField.text, 33)];
};

// "Set" from the controls (also the sliders, fields, quick buttons and presets).
RK.anim.applyControls = function () {
    var comp = RK.comp(true);
    if (!comp) return 0;
    var U = RK.anim.ui, lock = U.lock.getValue();
    var In = parseInt(U.inField.text, 10), Out = lock ? In : parseInt(U.outField.text, 10);
    var inOn = U.inOn.getValue() || lock, outOn = U.outOn.getValue() || lock;
    if ((inOn && !(In > 0)) || (outOn && !(Out > 0))) return 0;
    var speed = parseFloat(String(U.speedField.text).replace(",", "."));
    if (isNaN(speed)) speed = 0;
    return RK.anim.applyEase(comp, {
        speed: U.speedOn.getValue() ? speed : null,
        inf: inOn ? Math.min(100, In) : null,
        outf: outOn ? Math.min(100, Out) : null
    });
};

RK.anim.apply = RK.anim.guard("Interpolation", function () { RK.anim.applyControls(); });

// Slider handlers: live while dragging with Interactive update, else on release; never twice
// for one value.
RK.anim.slide = function (s, live) {
    if (live) {
        if (!RK.anim.interactive()) return;
        RK.anim.resetPresets();
        s.__applied = s.value;
        RK.anim.apply();
        return;
    }
    var done = s.__applied === s.value;
    s.__applied = undefined;
    RK.anim.resetPresets();
    if (!done) RK.anim.apply();
};

RK.anim.choosePreset = function () {
    var U = RK.anim.ui, sel = U.presets.selection;
    if (!sel || sel.index === 0) return;
    var p = RK.anim.parsePreset(sel.text);
    if (!p) return;
    RK.anim.setSpeed(p.speed);
    RK.anim.setInfluence("in", p.inf);
    RK.anim.setInfluence("out", p.outf);
    if (p.inf !== p.outf) RK.anim.setLock(false);
    RK.anim.apply();
};

RK.anim.getControls = function () {
    var comp = RK.comp();
    if (!comp) return;
    var g = RK.anim.getEase(comp);
    if (!g) {
        RK.msg("Select a keyframe first: its speed and influences are read.");
        return;
    }
    RK.anim.setSpeed(g.speed);
    RK.anim.setInfluence("in", g.inf);
    RK.anim.setInfluence("out", g.outf);
    if (Math.abs(g.inf - g.outf) > 1e-6) RK.anim.setLock(false);
};

// A row of the tab: centred unless `fill`.
RK.anim.row = function (parent, fill) {
    var r = parent.add("group");
    r.orientation = "row";
    r.alignChildren = ["left", "center"];
    r.alignment = [fill ? "fill" : "center", "top"];
    r.spacing = 4;
    return r;
};

RK.anim.buildTab = function (page) {
    var U = RK.anim.ui = {}, W = RK.PAGE_W;
    U.buttons = !!RK.pref("influenceButtons", false);
    page.orientation = "column";
    page.alignChildren = ["center", "top"];
    page.spacing = 2;
    var body = page.add("group");
    body.orientation = "column";
    body.alignChildren = ["fill", "top"];
    body.spacing = 3;
    body.preferredSize = [W, -1];

    // 1. keyframe types
    var r1 = RK.anim.row(body);
    var tips = {
        roving: "Roving: the selected keys (or a new key at the current time) rove in time; Position, Anchor Point and points only",
        linear: "Linear Interpolation: the selected keys, or a new key at the current time on each selected property",
        easeIn: "Ease In: eases into the selected keys (or a new key) with the In influence",
        easeOut: "Ease Out: eases out of the selected keys (or a new key) with the Out influence",
        easyEase: "Easy Ease: eases in and out of the selected keys (or a new key) with the In and Out influences",
        autoBezier: "Auto Bezier: smooth keys",
        hold: "Hold: the selected keys (or a new key) hold their value"
    };
    var types = ["roving", "linear", "easeIn", "easeOut", "easyEase", "autoBezier", "hold"];
    for (var i = 0; i < types.length; i++) {
        (function (type) {
            RK.ui.flat(r1, {
                icon: RK.anim.I[type], tip: tips[type], w: 30, h: 22, center: true, name: "anim." + type,
                onClick: RK.anim.guard(RK.anim.KEY[type], function () {
                    var comp = RK.comp();
                    if (!comp) return;
                    var f = RK.anim.typeInfluences();
                    RK.anim.keyType(comp, type, f[0], f[1]);
                })
            });
        })(types[i]);
    }

    // 2. presets
    var r2 = RK.anim.row(body);
    U.presets = r2.add("dropdownlist", undefined, ["Presets"]);
    U.presets.preferredSize = [W - 2 * 26 - 8, 22];
    U.presets.helpTip = "Interpolation presets: speed - in/out influences. Choosing one sets it on the selected keys.";
    RK.anim.fillPresets(RK.anim.presets());
    U.presets.onChange = RK.anim.guard("Presets", RK.anim.choosePreset);
    RK.ui.flat(r2, {
        icon: RK.I.plus, iconW: 20, w: 26, h: 22, center: true, name: "anim.presetAdd", tip: "Add the speed and influences as a preset",
        onClick: RK.anim.guard("Presets", function () {
            RK.anim.fillPresets(RK.anim.addPreset(RK.anim.formatPreset(U.speedField.text, U.inField.text, U.lock.getValue() ? U.inField.text : U.outField.text)));
        })
    });
    RK.ui.flat(r2, {
        icon: RK.I.minus, iconW: 20, w: 26, h: 22, center: true, name: "anim.presetRemove", tip: "Remove the chosen preset",
        onClick: RK.anim.guard("Presets", function () {
            var s = U.presets.selection;
            if (!s || s.index === 0) return;
            RK.anim.fillPresets(RK.anim.removePreset(s.index - 1));
        })
    });

    // 3-5. speed and influences: toggle, slider (or quick buttons), field
    var fieldW = 50, sliderW = W - 30 - fieldW - 2 * 4;
    var line = function (which, icon, tip, value, on, lo, hi) {
        var r = RK.anim.row(body);
        U[which + "On"] = RK.ui.toggle(r, { icon: icon, tip: tip, value: on, name: "anim." + which + "On", onChange: function () { RK.anim.syncRows(); } });
        if (which !== "speed" && U.buttons) {
            var g = r.add("group");
            g.spacing = 2;
            g.preferredSize = [sliderW, 22];
            U[which + "Buttons"] = [];
            var vals = [10, 25, 50, 75, 90, 100];
            for (var k = 0; k < vals.length; k++) {
                (function (v) {
                    U[which + "Buttons"].push(RK.ui.flat(g, {
                        label: String(v), keepLabel: true, w: 33, h: 22, center: true, name: "anim." + which + v, tip: tip + ": " + v + " %",
                        onClick: function () {
                            RK.anim.resetPresets();
                            U[which + "Field"].text = String(v);
                            if (which === "in" && U.lock.getValue()) U.outField.text = String(v);
                            RK.anim.apply();
                        }
                    }));
                })(vals[k]);
            }
        } else {
            var s = r.add("slider", undefined, value, lo, hi);
            s.preferredSize = [sliderW, 22];
            s.helpTip = tip;
            s.onChanging = function () {
                U[which + "Field"].text = String(Math.round(this.value));
                if (which === "in" && U.lock.getValue()) {
                    U.outField.text = String(Math.round(this.value));
                    if (U.outSlider) U.outSlider.value = this.value;
                }
                RK.anim.slide(this, true);
            };
            s.onChange = function () {
                U[which + "Field"].text = String(Math.round(this.value));
                if (which === "in" && U.lock.getValue()) {
                    U.outField.text = String(Math.round(this.value));
                    if (U.outSlider) U.outSlider.value = this.value;
                }
                RK.anim.slide(this, false);
            };
            U[which + "Slider"] = s;
        }
        var f = RK.ui.number(r, value, fieldW, tip);
        f.onChanging = function () {
            var v = parseInt(this.text, 10);
            if (isNaN(v) || v === 0) return;
            if (U[which + "Slider"]) U[which + "Slider"].value = which === "speed" ? RK.anim.clamp(v, lo, hi) : RK.anim.clamp(v, 1, 100);
            if (which === "in" && U.lock.getValue()) RK.anim.setInfluence("out", v);
            RK.anim.resetPresets();
            RK.anim.apply();
        };
        if (which === "speed") {
            f.onChange = function () {
                var v = parseFloat(String(this.text).replace(",", "."));
                if (!isNaN(v)) U.speedSlider.value = RK.anim.clamp(v, lo, hi);
                RK.anim.apply();
            };
        }
        f.name = "anim." + which + "Field";
        U[which + "Field"] = f;
        return r;
    };
    line("speed", RK.anim.I.speed, "Speed", 0, false, -1000, 1000);
    line("in", RK.anim.I.inInf, "Ease In influence", 33, true, 1, 100);
    U.outRow = line("out", RK.anim.I.outInf, "Ease Out influence", 33, true, 1, 100);

    // 6. lock, get, set
    var r6 = RK.anim.row(body);
    U.lock = RK.ui.toggle(r6, {
        icon: RK.anim.I.lockInf, tip: "Lock the influences: Out follows In", value: true, name: "anim.lock",
        onChange: function (v) { RK.anim.setLock(v); }
    });
    RK.ui.flat(r6, { label: "Get", icon: RK.anim.I.get, name: "anim.get", tip: "Get the speed and influences of the first selected keyframe", onClick: RK.anim.guard("Get", RK.anim.getControls) });
    RK.ui.flat(r6, { label: "Set", icon: RK.I.ok, name: "anim.set", tip: "Set the speed and influences on the selected keyframes", onClick: RK.anim.apply });

    // 7. spatial interpolation
    RK.ui.separator(body, "Spatial interpolation", W);
    var r7 = RK.anim.row(body);
    r7.spacing = 12;
    var spatial = function (mode, label, icon, tip) {
        RK.ui.flat(r7, {
            label: label, icon: icon, tip: tip, name: "anim.spatial." + mode,
            onClick: RK.anim.guard("Spatial interpolation", function () {
                var comp = RK.comp();
                if (comp) RK.anim.spatial(comp, mode);
            })
        });
    };
    spatial("linear", "Linear", RK.anim.I.spLinear, "Linear spatial interpolation: straight motion path through the selected keys");
    spatial("bezier", "Bezier", RK.anim.I.spBezier, "Bezier spatial interpolation: smooth motion path through the selected keys");

    // 8. tools
    RK.ui.separator(body, "Tools", W);
    var r8 = RK.anim.row(body);
    r8.spacing = 12;
    RK.ui.flat(r8, {
        label: "Morpher", icon: RK.anim.I.morpher, name: "anim.morpher",
        tip: "Create Morpher: one slider on the first selected layer that times all the selected properties (in frames)",
        onClick: RK.anim.guard("Morpher", function () {
            var comp = RK.comp();
            if (comp) RK.anim.morpher(comp, !!U.morphKeys.value);
        })
    });
    U.morphKeys = RK.ui.check(r8, "Keyframes", true, "Add keyframes to the Morpher, one at each keyframe of the properties it drives");
    U.morphKeys.preferredSize = [100, 18];

    // 9-10. tool buttons: two columns, then Import rig
    var cols = body.add("group");
    cols.alignment = ["center", "top"];
    cols.alignChildren = ["left", "top"];
    cols.spacing = 4;
    var ctl = RK.tools.controllers;
    var colW = function (labels) {
        if (RK.ui.expert) return RK.ICON_BOX + 4;
        var m = 0;
        for (var k = 0; k < labels.length; k++) m = Math.max(m, RK.textWidth(labels[k], RK.FONT));
        return RK.ICON_BOX + 4 + Math.ceil(m) + 8;
    };
    var left = cols.add("group"), right = cols.add("group");
    left.orientation = right.orientation = "column";
    left.alignChildren = right.alignChildren = ["left", "top"];
    left.spacing = right.spacing = 0;
    var wl = colW([RK.tools.copyanim.label, RK.tools.celanim.label]);
    var wr = colW([RK.tools.pasteanim.label, ctl ? ctl.label : "Controllers"]);
    RK.ui.toolButton(left, "copyanim", wl);
    RK.ui.toolButton(left, "celanim", wl);
    RK.ui.toolButton(right, "pasteanim", wr);
    RK.ui.flat(right, {
        label: ctl ? ctl.label : "Controllers", icon: ctl ? ctl.icon : RK.I.controllers, w: wr, name: "tool.controllers",
        tip: ctl ? ctl.tip : "Controllers", onClick: function () { RK.ui.runTool("controllers"); }
    });
    var r10 = RK.anim.row(body);
    RK.ui.toolButton(r10, "importrig");

    RK.anim.syncRows();
};

RK.ui.addTab("animation", "Animation", RK.I.tabAnim, "Animation", function (page) { RK.anim.buildTab(page); });

// ---------------------------------------------------------------------------- Cel animation page

RK.anim.celPage = function (body) {
    var S = RK.anim.celState, W = RK.PAGE_W, U = {};
    var labelW = 100, fieldW = 44, sliderW = W - labelW - fieldW - 2 * 4;
    var comp = function () { return RK.comp(); };
    var apply = function () {
        var c = comp();
        if (!c) return;
        RK.anim.onion(c, { active: S.onion, dur: S.dur, expo: S.expo, inOp: S.inOn ? S.inOp : 0, outOp: S.outOn ? S.outOp : 0 });
    };
    var act = function (label, fn) { return RK.anim.guard(label, fn); };
    var sync = function () {
        U.dur.enabled = U.frames.enabled = U.apply.enabled = S.onion;
        U.inCheck.enabled = U.outCheck.enabled = S.onion;
        U.inSlider.enabled = U.inField.enabled = S.onion && S.inOn;
        U.outSlider.enabled = U.outField.enabled = S.onion && S.outOn;
    };

    var r1 = RK.anim.row(body, true);
    U.single = RK.ui.check(r1, "Single Layer", S.single, "If enabled, creates cels as new Paint effects on a single layer");
    U.single.preferredSize = [labelW + 30, 18];
    U.single.onClick = function () { S.single = !!this.value; };
    RK.ui.button(r1, "New Cel.", function () {
        var c = comp();
        if (c) RK.anim.newCel(c, S.single);
    }, "Creates a new animation cel", 90);
    RK.ui.separator(body);

    var r2 = RK.anim.row(body, true);
    U.onion = RK.ui.check(r2, "Onion skin", S.onion, "Activates onion skin");
    U.onion.preferredSize = [labelW, 18];
    U.dur = RK.ui.number(r2, S.dur, fieldW, "Onion skin duration (frames)");
    U.frames = RK.ui.label(r2, "frames");
    U.onion.onClick = act("Onion skin", function () {
        S.onion = !!this.value;
        sync();
        apply();
    });
    U.dur.onChange = act("Onion skin", function () {
        S.dur = RK.ui.int(U.dur, S.dur, 1, 1000);
        apply();
    });

    // In / Out opacity rows. Ticking one removes the onion skin and makes it again with the new
    // setting (one undo step); one of the two always stays on.
    var opacityRow = function (which, label) {
        var r = RK.anim.row(body, true);
        var on = which + "On", op = which + "Op";
        var cb = RK.ui.check(r, label, S[on], label + " of the onion skin");
        cb.preferredSize = [labelW, 18];
        var s = r.add("slider", undefined, S[op], 1, 100);
        s.preferredSize = [sliderW, 22];
        s.helpTip = label;
        var f = RK.ui.number(r, S[op], fieldW, label);
        s.onChanging = function () { f.text = String(Math.round(this.value)); };
        s.onChange = act("Onion skin", function () {
            f.text = String(Math.round(this.value));
            S[op] = Math.round(this.value);
            apply();
        });
        f.onChange = act("Onion skin", function () {
            S[op] = RK.ui.int(f, S[op], 1, 100);
            s.value = S[op];
            apply();
        });
        cb.onClick = act("Onion skin", function () {
            S[on] = !!this.value;
            var other = which === "in" ? "out" : "in";
            if (!S.inOn && !S.outOn) {
                S[other + "On"] = true;
                U[other + "Check"].value = true;
            }
            var c = comp();
            if (!c) {
                sync();
                return;
            }
            RK.undo(RK.NAME + " - Update Onion Skin", function () {
                var o = { active: false, dur: S.dur, expo: S.expo, inOp: 0, outOp: 0 };
                RK.anim.onion(c, o);
                S.onion = true;
                U.onion.value = true;
                apply();
            });
            sync();
        });
        U[which + "Check"] = cb;
        U[which + "Slider"] = s;
        U[which + "Field"] = f;
    };
    opacityRow("in", "In Opacity");
    opacityRow("out", "Out Opacity");

    var getRow = RK.anim.row(body, true);
    RK.ui.flat(getRow, {
        label: "Get current Onion Skin", icon: RK.anim.I.getOnion, name: "cel.get", tip: "Read the onion skin of the cel's strokes into these settings",
        onClick: act("Onion skin", function () {
            var c = comp();
            if (!c) return;
            var g = RK.anim.onionGet(c);
            S.onion = g.active;
            U.onion.value = g.active;
            if (g.active) {
                S.dur = g.dur;
                U.dur.text = String(g.dur);
                S.outOn = g.outOp > 0;
                S.inOn = g.inOp > 0;
                U.outCheck.value = S.outOn;
                U.inCheck.value = S.inOn;
                if (S.outOn) {
                    S.outOp = Math.round(g.outOp);
                    U.outField.text = String(S.outOp);
                    U.outSlider.value = S.outOp;
                }
                if (S.inOn) {
                    S.inOp = Math.round(g.inOp);
                    U.inField.text = String(S.inOp);
                    U.inSlider.value = S.inOp;
                }
            }
            sync();
        })
    });
    var applyRow = RK.anim.row(body, true);
    U.apply = RK.ui.flat(applyRow, {
        label: "Apply Onion Skin", icon: RK.anim.I.onion, name: "cel.apply",
        tip: "Hold the new strokes for the exposure and apply (or remove) the onion skin", onClick: act("Onion skin", apply)
    });
    RK.ui.separator(body);

    var r6 = RK.anim.row(body);
    var step = function (dir) {
        return act("Onion skin", function () {
            var c = comp();
            if (!c) return;
            S.expo = RK.ui.int(U.expo, S.expo, 1, 1000);
            c.time = c.time + dir * S.expo * c.frameDuration;
            apply();
        });
    };
    RK.ui.flat(r6, { icon: RK.I.prev, w: 30, h: 22, center: true, name: "cel.prev", tip: "Previous frame (back by the exposure)", onClick: step(-1) });
    RK.ui.label(r6, "Exposure");
    U.expo = RK.ui.number(r6, S.expo, fieldW, "Exposure (frames): how long new strokes are held");
    U.expo.onChange = function () { S.expo = RK.ui.int(U.expo, S.expo, 1, 1000); };
    RK.ui.flat(r6, { icon: RK.I.next, w: 30, h: 22, center: true, name: "cel.next", tip: "Next frame (on by the exposure)", onClick: step(1) });

    sync();
    RK.ui.bottomRow(body, [RK.ui.backItem()]);
};

// ---------------------------------------------------------------------------- Import rig page

RK.anim.importPage = function (body) {
    var W = RK.PAGE_W, list = [];
    RK.ui.label(body, "Rigged composition to import:");
    var r = RK.anim.row(body, true);
    var drop = r.add("dropdownlist", undefined, []);
    drop.preferredSize = [W - 34, 22];
    drop.helpTip = "The rig to import";
    var fill = function () {
        list = RK.anim.comps();
        drop.removeAll();
        for (var i = 0; i < list.length; i++) drop.add("item", list[i].label);
        drop.selection = null;
    };
    RK.ui.flat(r, { icon: RK.anim.I.refresh, w: 30, h: 22, center: true, name: "rig.refresh", tip: "Refresh the list of compositions", onClick: fill });
    RK.ui.label(body, "Name of this instance:");
    var name = RK.ui.text(body, "Must be unique!", W, "The instance name: the prefix of the copied comps and of the new controllers");
    var chosen = function () { return drop.selection ? list[drop.selection.index].comp : null; };
    fill();
    RK.ui.bottomRow(body, [
        RK.ui.backItem("Cancel"),
        {
            label: "Duplicate", icon: RK.anim.I.duplicate, name: "rig.duplicate", tip: "Duplicate selected rig (a renamed deep copy, not imported)",
            onClick: RK.ui.action("Duplicate", function () {
                var inst = RK.trim(name.text), d = RK.anim.duplicateRig(chosen(), inst);
                if (!d) return false;
                RK.msg("Duplicated \"" + chosen().name + "\" as \"" + d.comp.name + "\".");
            }, true)
        },
        {
            label: "Import", icon: RK.I.ok, name: "rig.import", tip: "Import selected rig in the active comp",
            onClick: RK.ui.action("Import", function () {
                var C = RK.comp();
                if (!C) return false;
                var inst = RK.trim(name.text), R = chosen(), res = RK.anim.importRig(C, R, inst);
                if (!res) return false;
                RK.msg("Imported \"" + R.name + "\" as \"" + inst + "\": " + res.controllers + (res.controllers === 1 ? " controller." : " controllers.") +
                    (res.errors.length ? "\n" + res.errors.length + " properties could not be linked: " + res.errors[0] : ""));
            }, true)
        }
    ]);
};

// ============================================================================ Cameras tab
//
// Control Cam (three controllers drive a two-node camera), Scale Z-Link (layers keep their
// apparent size as they move in depth), 2D Multiplane (a fake camera made of parallax planes)
// and Import TVPaint Cam. Expression helpers and world positions come from 52_rigtools.js.

RK.cam = {};

RK.cam.I = {
    // a camera in a framed view
    controlcam: [
        ["L", 1, 4.5, 1, 1, 4.5, 1], ["L", 19.5, 1, 23, 1, 23, 4.5],
        ["L", 23, 11.5, 23, 15, 19.5, 15], ["L", 4.5, 15, 1, 15, 1, 11.5],
        ["F", 5, 5, 9, 6], ["P", 14, 8, 19.5, 4.5, 19.5, 11.5]
    ],
    // a camera's view cone with two layers, the farther one bigger
    zlink: [
        ["F", 1, 6, 4, 4], ["P", 5, 8, 8, 6, 8, 10],
        ["L", 8, 8, 23, 1.5], ["L", 8, 8, 23, 14.5],
        ["L", 14, 5.2, 14, 10.8], ["L", 20.5, 2.6, 20.5, 13.4]
    ],
    // three planes, one above the other
    multiplane: [
        ["Z", 2, 1.5, 19, 1.5, 22, 4.5, 5, 4.5],
        ["Z", 2, 6.5, 19, 6.5, 22, 9.5, 5, 9.5],
        ["Z", 2, 11.5, 19, 11.5, 22, 14.5, 5, 14.5]
    ],
    // a paint stroke and a camera
    tvpaint: [
        RK.op("L", RK.bez(1.5, 14.5, 3, 4, 7, 15, 10, 3, 10)),
        ["F", 13, 5, 6, 6], ["P", 19, 8, 23, 5, 23, 11]
    ]
};

RK.cam.pad2 = function (i) {
    return i < 10 ? "0" + i : String(i);
};

// ---------------------------------------------------------------------------- Control Cam

// World position of a camera and of its point of interest, now.
RK.cam.cameraWorld = function (K) {
    var tr = K.transform, t = K.containingComp.time;
    var pos = tr.property("ADBE Position"), poi = tr.property("ADBE Anchor Point");
    if (!K.parent && !pos.dimensionsSeparated) {
        var a = pos.valueAtTime(t, false), b = poi ? poi.valueAtTime(t, false) : null;
        if (b) return { pos: [a[0], a[1], a[2]], poi: [b[0], b[1], b[2]] };
    }
    var w = RK.rt.probeWorld(K);
    var q = poi ?
        RK.probe(K, "var q = L.transform.pointOfInterest.valueAtTime(T);\nif (L.hasParent) q = L.parent.toWorld(q, T);\n[q[0], q[1], q[2], 0]") :
        RK.probe(K, "var q = L.toWorld([0, 0, 1000], T);\n[q[0], q[1], q[2], 0]");
    return { pos: w, poi: [q[0], q[1], q[2]] };
};

// A controller's anchor point in the world, in this layer's parent space (pick: "" or "[d]").
RK.cam.worldExpr = function (ctrlName, anchor, pick) {
    var x = RK.xHead("control cam") + RK.rt.XFAST;
    x += RK.rt.readLayer("C", ctrlName);
    x += RK.rt.readXf("cX", "C");
    x += RK.rt.readParentXf("pX", "rkMe");
    x += "if (rk_wait) { value; } else {\n";
    x += "    var w = rkW(cX, " + RK.xValue([anchor[0], anchor[1], anchor.length > 2 ? anchor[2] : 0]) + ");\n";
    x += "    (pX === false ? w : rkFW(pX, w))" + pick + ";\n}\n";
    return x;
};

// This property's own value plus the same transform property of a controller.
RK.cam.addExpr = function (ctrlName, id, api) {
    var x = RK.xHead("control cam") + RK.rt.XFAST;
    x += RK.rt.readLayer("C", ctrlName);
    x += RK.rt.readTr("o", "C", id, api);
    x += "if (rk_wait) { value; } else { value + o; }\n";
    return x;
};

RK.cam.driveCamera = function (K, posC, target) {
    var tr = K.transform;
    var pa = posC.transform.anchorPoint.value, ta = target.transform.anchorPoint.value;
    var pos = tr.property("ADBE Position");
    if (pos.dimensionsSeparated) {
        for (var d = 0; d < 3; d++) RK.setExpr(tr.property("ADBE Position_" + d), RK.cam.worldExpr(posC.name, pa, "[" + d + "]"));
    } else {
        RK.setExpr(pos, RK.cam.worldExpr(posC.name, pa, ""));
    }
    var poi = tr.property("ADBE Anchor Point");
    if (poi) RK.setExpr(poi, RK.cam.worldExpr(target.name, ta, ""));
    RK.setExpr(tr.property("ADBE Orientation"), RK.cam.addExpr(posC.name, "orientation", "orientation"));
    RK.setExpr(tr.property("ADBE Rotate X"), RK.cam.addExpr(posC.name, "rotationX", "xRotation"));
    RK.setExpr(tr.property("ADBE Rotate Y"), RK.cam.addExpr(posC.name, "rotationY", "yRotation"));
    RK.setExpr(tr.property("ADBE Rotate Z"), RK.cam.addExpr(posC.name, "rotation", "zRotation"));
};

// Three 3D controllers for camera K: "<K> target" (the point of interest), "<K> position" (the
// camera, which keeps looking at the target) and "Control_<K>" moving both. The camera follows
// them through expressions (world positions, converted into its parent's space) and is locked.
RK.cam.controlCam = function (comp, K) {
    var N = K.name;
    var W = RK.cam.cameraWorld(K);
    var names = RK.layerNames(comp);
    var make = function (name, flags, at) {
        var c = RK.ctrl.create(comp, null, { flags: flags, name: RK.uniqueName(comp, name, names), select: false });
        c.threeDLayer = true;
        c.transform.position.setValue([at[0], at[1], at[2]]);
        return c;
    };
    var target = make(N + " target", { rotation: false, x: true, y: true, scale: false }, W.poi);
    var posC = make(N + " position", { rotation: false, x: true, y: true, scale: false }, W.pos);
    var main = make("Control_" + N, { camera: true }, W.pos);
    posC.parent = main;
    target.parent = main;
    RK.withUnlocked(K, function () { RK.cam.driveCamera(K, posC, target); });
    K.locked = true;
    RK.selectOnly(comp, [main]);
    return { main: main, position: posC, target: target };
};

RK.tool("controlcam", {
    label: "Control Cam", icon: RK.cam.I.controlcam,
    tip: "Creates controllers for the selected camera: a target, a position (always looking at the target) and a main controller moving both.",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        var sel = comp.selectedLayers;
        if (sel.length !== 1) {
            RK.msg("No camera selected: select the camera.");
            return;
        }
        if (!(sel[0] instanceof CameraLayer)) {
            RK.msg("The selected layer is not a camera: select the camera.");
            return;
        }
        RK.undo(RK.NAME + " - Camera Controller", function () { RK.cam.controlCam(comp, sel[0]); });
    }
});

// ---------------------------------------------------------------------------- Scale Z-Link

RK.cam.ZTAG = "scale z-link";

// The camera the links use: the first camera among the layers, else the comp's topmost one.
RK.cam.findCamera = function (comp, layers) {
    var i;
    for (i = 0; i < layers.length; i++) if (layers[i] instanceof CameraLayer) return layers[i];
    for (i = 1; i <= comp.numLayers; i++) if (comp.layer(i) instanceof CameraLayer) return comp.layer(i);
    return null;
};

// Scale = Scale slider * distance to the camera / distance at link time, on all three axes.
// The camera's world position is read from its Position (and its parent), not from its whole
// transform: a rigged camera's rotation expressions then don't run again for every layer.
RK.cam.zLinkExpr = function (camName, d0, separated, camSeparated) {
    var x = RK.xHead(RK.cam.ZTAG) + RK.rt.XFAST;
    x += RK.rt.readFx("s", "rkMe", "Scale");
    x += RK.rt.readLayer("C", camName);
    if (camSeparated) {
        x += RK.rt.readTr("cx", "C", "positionX", "xPosition");
        x += RK.rt.readTr("cy", "C", "positionY", "yPosition");
        x += RK.rt.readTr("cz", "C", "positionZ", "zPosition");
    } else {
        x += RK.rt.readTr("cp", "C", "position", "position");
    }
    x += RK.rt.readParentXf("cX", "C");
    x += RK.rt.readParentXf("pX", "rkMe");
    if (separated) {
        x += RK.rt.readTr("px", "rkMe", "positionX", "xPosition");
        x += RK.rt.readTr("py", "rkMe", "positionY", "yPosition");
        x += RK.rt.readTr("pz", "rkMe", "positionZ", "zPosition");
    } else {
        x += RK.rt.readTr("p", "rkMe", "position", "position");
    }
    x += "if (rk_wait) { value; } else {\n";
    if (separated) x += "    var p = [px, py, pz];\n";
    if (camSeparated) x += "    var cp = [cx, cy, cz];\n";
    // a layer's anchor point (a camera's eye) is in the world where its Position is in its parent
    x += "    var o = pX === false ? p : rkW(pX, p);\n";
    x += "    var c = cX === false ? cp : rkW(cX, cp);\n";
    x += "    var k = s == 0 ? 0 : s * length(c, o) / " + RK.num(d0) + ";\n";
    x += "    [k, k, k];\n}\n";
    return x;
};

RK.cam.zLinkLayer = function (comp, L, cam, camPos) {
    var sc = L.transform.scale;
    if (RK.isOurExpr(sc, RK.cam.ZTAG)) {
        // unlink: keep the size it shows now
        var v = sc.valueAtTime(comp.time, false);
        sc.expression = "";
        var e = RK.findEffect(L, "Scale");
        if (e) e.remove();
        if (sc.numKeys === 0) sc.setValue([v[0], v[1], v.length > 2 ? v[2] : 100]);
        return "unlinked";
    }
    if (!RK.is3D(L)) {
        try {
            L.threeDLayer = true;
        } catch (err) {}
        if (!RK.is3D(L)) return "skipped";
    }
    var d0 = RK.dist(RK.rt.worldPos(L), camPos);
    if (d0 < 0.0001) d0 = 1;
    var s0 = sc.valueAtTime(comp.time, false)[0];
    var fx = RK.addSlider(L, "Scale", s0);
    RK.fxOff(L, fx);
    RK.setExpr(sc, RK.cam.zLinkExpr(cam.name, d0, L.transform.position.dimensionsSeparated, cam.transform.property("ADBE Position").dimensionsSeparated));
    return "linked";
};

// Link (or unlink, when linked) every layer; returns {linked, unlinked, skipped} counts.
RK.cam.zLink = function (comp, layers) {
    layers = layers || comp.selectedLayers;
    var out = { linked: 0, unlinked: 0, skipped: 0 };
    if (!layers.length) {
        RK.msg("No layer is selected. Select the layers to link to the camera.");
        return out;
    }
    var cam = RK.cam.findCamera(comp, layers);
    if (!cam) {
        RK.msg("There is no camera in this comp: add a camera first.");
        return out;
    }
    var camPos = RK.rt.worldPos(cam);
    for (var i = 0; i < layers.length; i++) {
        var L = layers[i];
        if (L instanceof CameraLayer || L instanceof LightLayer) continue;
        out[RK.withUnlocked(L, function () { return RK.cam.zLinkLayer(comp, L, cam, camPos); })]++;
    }
    return out;
};

RK.tool("zlink", {
    label: "Scale Z-Link", icon: RK.cam.I.zlink,
    tip: "Links the selected layers' scale to their distance from the camera, so they keep their apparent size when they move in depth. Run it again on a linked layer to remove the link.",
    run: function () {
        var comp = RK.comp();
        if (!comp) return;
        RK.undo(RK.NAME + " - Scale Z-Link", function () { RK.cam.zLink(comp); });
    }
});

// ---------------------------------------------------------------------------- 2D Multiplane
//
// n planes (nulls, the camera plane a controller), each with a zero. Every zero follows the
// camera's Multiplane effect: Pan (position), Tilt (rotation), Zoom (+ scale) and Truck In Out
// (+ scale times the plane's influence). Each plane moves by the camera controller's position
// times its "Camera Influence": above 1 in front of the camera plane, below 1 behind it.

RK.cam.GROUP = "Multiplane";

// Influence of plane i (1 = bottom) with the camera plane c.
RK.cam.influence = function (i, c) {
    return i > c ? 1 + 0.2 * (i - c) : (i + 1) / (c + 1);
};

RK.cam.mpRead = function (v, param) {
    return RK.rt.readFx(v, "C", RK.fxg(RK.cam.GROUP, param));
};

// A zero's expressions. kPlane: the plane's name (its influence is read from it), or null for
// the camera plane (influence 1).
RK.cam.zeroExprs = function (camName, kPlane) {
    var head = RK.xHead("multiplane") + RK.rt.XFAST + RK.rt.readLayer("C", camName);
    var rot = head + RK.cam.mpRead("tilt", "Tilt") + "if (rk_wait) { value; } else { tilt; }\n";
    var pos = head + RK.cam.mpRead("pan", "Pan") + "if (rk_wait) { value; } else { [pan[0], pan[1]]; }\n";
    var sc = head + RK.cam.mpRead("zoom", "Zoom") + RK.cam.mpRead("truck", "Truck In Out");
    if (kPlane) sc += RK.rt.readLayer("K", kPlane) + RK.rt.readFx("k", "K", "Camera Influence");
    else sc += "var k = 1;\n";
    sc += "if (rk_wait) { value; } else { var a = zoom + truck * k; [value[0] + a, value[1] + a]; }\n";
    return { rotation: rot, position: pos, scale: sc };
};

RK.cam.planeExpr = function (camName) {
    var x = RK.xHead("multiplane") + RK.rt.XFAST;
    x += RK.rt.readLayer("C", camName);
    x += RK.rt.readTr("cp", "C", "position", "position");
    x += RK.rt.readFx("k", "rkMe", "Camera Influence");
    x += "if (rk_wait) { value; } else { [cp[0] * k, cp[1] * k]; }\n";
    return x;
};

RK.cam.rigZero = function (z, ex) {
    RK.setExpr(RK.rotProp(z), ex.rotation);
    RK.setExpr(z.transform.scale, ex.scale);
    RK.setExpr(z.transform.position, ex.position);
};

// o: {layers: the layers to link (or null), n: number of planes without layers, c: camera
// plane (1 = bottom)}. Returns {camera, planes (bottom first), zeros} or null.
RK.cam.multiplane = function (comp, o) {
    var given = o.layers ? o.layers.slice() : [];
    given.sort(function (a, b) { return a.index - b.index; });   // topmost first
    var n = given.length ? given.length : Math.floor(Number(o.n));
    if (!(n >= 1)) {
        RK.msg("Set the number of layers (at least one).");
        return null;
    }
    var c = Math.floor(Number(o.c));
    if (!(c >= 1 && c <= n)) c = Math.ceil(n / 2);
    var st = RK.ctrl.settings(), names = RK.layerNames(comp);
    var planes = [], zeros = [], cam = null, i;
    // Plane i (1 = the bottom, background one) is created i-th, so it ends below the later
    // ones; the given layer parented to it is the i-th from the bottom, and names it.
    for (i = 1; i <= n; i++) {
        var gl = given.length ? given[n - i] : null;
        var base = "L" + RK.cam.pad2(i) + (gl ? "_" + gl.name : "");
        var L;
        if (i === c) {
            L = st.type === "null" ? comp.layers.addNull() : RK.ctrl.create(comp, null, { flags: { camera: true }, select: false });
            L.name = RK.uniqueName(comp, base + " CAMERA", names);
            cam = L;
        } else {
            L = comp.layers.addNull();
            L.name = RK.uniqueName(comp, base, names);
        }
        planes.push(L);
        zeros.push(RK.zero(L, { unlocked: true }));
    }
    RK.addGroup(cam, RK.cam.GROUP, [
        { name: "Pan", type: "point", value: [comp.width / 2, comp.height / 2] },
        { name: "Zoom", type: "slider", value: 0 },
        { name: "Truck In Out", type: "slider", value: 0 },
        { name: "Tilt", type: "angle", value: 0 }
    ]);
    for (i = 1; i <= n; i++) {
        var P = planes[i - 1];
        if (i === c) {
            RK.cam.rigZero(zeros[i - 1], RK.cam.zeroExprs(cam.name, null));
            continue;
        }
        var fx = RK.addSlider(P, "Camera Influence", RK.cam.influence(i, c));
        RK.fxOff(P, fx);
        RK.setExpr(P.transform.position, RK.cam.planeExpr(cam.name));
        RK.cam.rigZero(zeros[i - 1], RK.cam.zeroExprs(cam.name, P.name));
    }
    for (i = 0; i < given.length; i++) {
        (function (g, p) { RK.withUnlocked(g, function () { g.parent = p; }); })(given[i], planes[n - 1 - i]);
    }
    for (i = 0; i < zeros.length; i++) zeros[i].locked = true;
    RK.selectOnly(comp, [cam]);
    return { camera: cam, planes: planes, zeros: zeros, c: c, n: n };
};

RK.cam.mpState = { link: false, n: 5, c: 3 };

RK.cam.multiplanePage = function (body) {
    var st = RK.cam.mpState;
    var comp = RK.comp(true);
    if (comp) {
        var sel = comp.selectedLayers;
        if (sel.length) {
            st.link = true;
            st.n = sel.length;
            st.c = Math.ceil(sel.length / 2);
        } else {
            st.link = false;
        }
    }
    var link = RK.ui.check(body, "Link selected layers", st.link, "One plane per selected layer, each layer parented to its plane");
    RK.ui.separator(body);
    var setN = RK.ui.check(body, "Set the number of layers", !st.link);
    var row = RK.ui.row(body);
    var sl = row.add("slider", undefined, Math.max(1, Math.min(10, st.n)), 1, 10);
    sl.preferredSize = [RK.PAGE_W - 60, 22];
    var nField = RK.ui.number(row, RK.cam.pad2(st.n), 50, "Number of planes");
    RK.ui.separator(body);
    var r2 = RK.ui.row(body, "Camera index:", 96);
    var cField = RK.ui.number(r2, RK.cam.pad2(st.c), 50, "The plane that is the camera (1 = the bottom one)");
    var enableN = function (on) {
        sl.enabled = on;
        nField.enabled = on;
    };
    link.onClick = function () {
        setN.value = !this.value;
        enableN(!this.value);
    };
    setN.onClick = function () {
        link.value = !this.value;
        enableN(this.value);
    };
    sl.onChanging = sl.onChange = function () {
        var v = Math.round(this.value);
        nField.text = RK.cam.pad2(v);
        cField.text = RK.cam.pad2(Math.ceil(v / 2));
    };
    nField.onChanging = function () {
        var v = parseInt(String(this.text).replace(/^\s*0+(?=\d)/, ""), 10);
        if (isNaN(v)) return;
        sl.value = Math.max(1, Math.min(10, v));
        cField.text = RK.cam.pad2(Math.ceil(Math.round(sl.value) / 2));
    };
    enableN(!st.link);
    RK.ui.bottomRow(body, [RK.ui.backItem("Cancel"), {
        label: "Multiplane", icon: RK.I.ok, tip: "Creates a 2D multiplane camera rig",
        onClick: RK.ui.action("Multiplane", function () {
            var c = RK.comp();
            if (!c) return false;
            st.link = link.value;
            st.n = RK.ui.int(nField, -1);
            st.c = RK.ui.int(cField, -1);
            var r = null;
            RK.undo(RK.NAME + " - Multiplane", function () {
                r = RK.cam.multiplane(c, { layers: st.link ? c.selectedLayers : null, n: st.n, c: st.c });
            });
            if (r) {
                st.n = r.n;
                st.c = r.c;
            }
            return !!r;
        }, true)
    }]);
};

RK.tool("multiplane", {
    label: "2D Multiplane", icon: RK.cam.I.multiplane,
    tip: "Creates multiplane controllers to use as a 2D camera (parallax planes driven by a camera controller).",
    page: function (body) { RK.cam.multiplanePage(body); }
});

// ---------------------------------------------------------------------------- Import TVPaint Cam
//
// A TVPaint camera file (.cpt) is text, one key=value per line: pointcount, point.<i>.x / .y /
// .zoom / .rotation, profileprof_linear, profileprof_numpoint, profileprof_point<j> = "u v".

RK.cam.parseCpt = function (text) {
    var lines = String(text).split(/\r\n|\r|\n/), pts = {}, prof = {}, count = 0, linear = false, nprof = 0, m, i;
    for (i = 0; i < lines.length; i++) {
        var kv = /^\s*([^=]+?)\s*=\s*(.*?)\s*$/.exec(lines[i]);
        if (!kv) continue;
        var k = kv[1].toLowerCase(), v = kv[2];
        if (k === "pointcount") count = parseInt(v, 10);
        else if ((m = /^point\.(\d+)\.(x|y|zoom|rotation)$/.exec(k))) {
            var p = pts[m[1]] || (pts[m[1]] = {});
            p[m[2]] = parseFloat(v);
        } else if (k === "profileprof_linear") linear = parseInt(v, 10) === 1;
        else if (k === "profileprof_numpoint") nprof = parseInt(v, 10);
        else if ((m = /^profileprof_point(\d+)$/.exec(k))) {
            var uv = v.split(/[\s,;]+/);
            prof[m[1]] = [parseFloat(uv[0]), parseFloat(uv[1])];
        }
    }
    if (!(count >= 1)) throw new Error("this file has no camera points (pointcount).");
    var out = { points: [], linear: linear, profile: [] };
    var num = function (x, d) { return typeof x === "number" && isFinite(x) ? x : d; };
    for (i = 0; i < count; i++) {
        var q = pts[String(i)];
        if (!q) throw new Error("the file counts " + count + " camera points, but point " + i + " is missing.");
        out.points.push({ x: num(q.x, 0), y: num(q.y, 0), zoom: num(q.zoom, 1), rotation: num(q.rotation, 0) });
    }
    for (i = 0; i < nprof; i++) {
        var r = prof[String(i)];
        if (r && isFinite(r[0]) && isFinite(r[1])) out.profile.push([Math.max(0, Math.min(1, r[0])), Math.max(0, Math.min(1, r[1]))]);
    }
    out.profile.sort(function (a, b) { return a[0] - b[0]; });
    return out;
};

RK.cam.readFile = function (path) {
    path = RK.trim(path).replace(/^["']+|["']+$/g, "");
    if (!path) throw new Error("paste the path of the camera file (.cpt) exported from TVPaint.");
    var f = new File(path), text = null, ok = false;
    try {
        ok = f.open("r");
        if (ok) text = f.read();
        f.close();
    } catch (e) {
        throw new Error("EffectCraft didn't let the script read \"" + path + "\". Turn on Edit > Preferences > Scripting & Expressions > Allow Scripts to Write Files and Access Network, or put the file in the project's folder.");
    }
    if (!ok || text === null) throw new Error("can't open \"" + path + "\": check the path.");
    return text;
};

// The Morpher time-remap: the property shows its own animation at Morpher frames.
RK.cam.morphExpr = function (xName, morpher) {
    var x = RK.xHead("tvpaint camera") + RK.rt.XFAST;
    if (xName) {
        x += RK.rt.readLayer("X", xName);
        x += RK.rt.readFx("m", "X", morpher);
    } else {
        x += RK.rt.readFx("m", "rkMe", morpher);
    }
    x += "if (rk_wait) { value; } else { valueAtTime(m * thisComp.frameDuration); }\n";
    return x;
};

// Apply camera `cam` (parseCpt) to layer X. o: {anchor: animate X's anchor point (else its
// position, through a "TVPaint Rotation & Scale" parent), autoParent}. A locked X is unlocked
// for the work and locked again.
RK.cam.applyTvp = function (comp, X, cam, o) {
    return RK.withUnlocked(X, function () { return RK.cam.applyTvpTo(comp, X, cam, o); });
};

RK.cam.applyTvpTo = function (comp, X, cam, o) {
    var tr = X.transform, D = comp.duration, fd = comp.frameDuration, i, d;
    var span = Math.max(0, D - fd), n = cam.points.length;
    var names = RK.layerNames(comp);
    var R = X;
    if (!o.anchor) {
        R = comp.layers.addNull();
        R.name = RK.uniqueName(comp, "TVPaint Rotation & Scale", names);
        // parented while R is still at rest, so X's keys below are in R's space as they are
        RK.withUnlocked(X, function () { X.parent = R; });
    }
    var M = o.anchor ? tr.property("ADBE Anchor Point") : tr.property("ADBE Position");
    var parts = !o.anchor && M.dimensionsSeparated ? [tr.property("ADBE Position_0"), tr.property("ADBE Position_1")] : [M];
    var cur = M.valueAtTime(comp.time, false);
    if (parts.length > 1) cur = [parts[0].valueAtTime(comp.time, false), parts[1].valueAtTime(comp.time, false), cur[2] || 0];
    var x0 = cam.points[0].x, y0 = cam.points[0].y;
    var times = [], idx = [];
    RK.withUnlocked(X, function () {
        for (i = 0; i < n; i++) {
            var p = cam.points[i], t = n > 1 ? span * i / (n - 1) : 0;
            var v = o.anchor ? [p.x, p.y] : [cur[0] + x0 - p.x, cur[1] + y0 - p.y];
            if (parts.length > 1) {
                parts[0].setValueAtTime(t, v[0]);
                parts[1].setValueAtTime(t, v[1]);
            } else {
                M.setValueAtTime(t, [v[0], v[1], cur.length > 2 ? cur[2] : 0]);
            }
            times.push(t);
        }
        var K = parts[0];
        for (i = 0; i < n; i++) idx.push(K.nearestKeyIndex(times[i]));
        if (parts.length === 1 && n > 1) {
            var zero = [0, 0, 0];
            for (i = 0; i < n; i++) {
                if (i === 0 || i === n - 1) K.setSpatialTangentsAtKey(idx[i], zero, zero);
                else K.setSpatialAutoBezierAtKey(idx[i], true);
            }
            // roving middle keys: constant speed along the path (the ends stay where they are)
            for (i = 1; i < n - 1; i++) K.setRovingAtKey(idx[i], true);
        }
        for (i = 0; i < n; i++) times[i] = K.keyTime(idx[i]);
    });
    var rot = RK.rotProp(R), sc = R.transform.scale;
    RK.withUnlocked(R, function () {
        for (i = 0; i < n; i++) {
            var s = (2 - cam.points[i].zoom) * 100;
            rot.setValueAtTime(times[i], cam.points[i].rotation);
            sc.setValueAtTime(times[i], [s, s, s]);
        }
    });
    // timing: the Morpher replays the path at the camera's own pace
    var mName = RK.uniqueEffectName(X, "Morpher");
    var mFx = RK.addSlider(X, mName, 0);
    RK.fxOff(X, mFx);
    var mp = mFx.property(1), prof = cam.profile;
    if (!prof.length) prof = [[0, 0], [1, 1]];
    var mi = [];
    for (i = 0; i < prof.length; i++) {
        mp.setValueAtTime(span * prof[i][0], span / fd * prof[i][1]);
    }
    for (i = 0; i < prof.length; i++) mi.push(mp.nearestKeyIndex(span * prof[i][0]));
    if (!cam.linear && cam.profile.length) {
        for (i = 0; i < mi.length; i++) {
            if (i === 0 || i === mi.length - 1) {
                mp.setTemporalEaseAtKey(mi[i], [new KeyframeEase(0, 33.33)], [new KeyframeEase(0, 33.33)]);
            } else {
                // (auto first: setting it clears continuity)
                mp.setInterpolationTypeAtKey(mi[i], KeyframeInterpolationType.BEZIER, KeyframeInterpolationType.BEZIER);
                mp.setTemporalAutoBezierAtKey(mi[i], true);
                mp.setTemporalContinuousAtKey(mi[i], true);
            }
        }
    }
    RK.withUnlocked(X, function () {
        for (d = 0; d < parts.length; d++) RK.setExpr(parts[d], RK.cam.morphExpr(null, mName));
        if (R === X) {
            RK.setExpr(rot, RK.cam.morphExpr(null, mName));
            RK.setExpr(sc, RK.cam.morphExpr(null, mName));
        }
    });
    if (R !== X) {
        RK.setExpr(rot, RK.cam.morphExpr(X.name, mName));
        RK.setExpr(sc, RK.cam.morphExpr(X.name, mName));
    }
    if (o.autoParent) {
        var all = RK.rt.layersOf(comp);
        for (i = 0; i < all.length; i++) {
            var L = all[i];
            if (L.id === X.id || L.id === R.id || L.parent) continue;
            (function (L) { RK.withUnlocked(L, function () { L.parent = X; }); })(L);
        }
    }
    return { layer: X, rs: R, morpher: mName };
};

// o: {mode: "null" | "precomp" | "selected", autoParent, anchor}. Returns applyTvp's result.
RK.cam.importTvp = function (comp, text, o) {
    var cam = RK.cam.parseCpt(text), X;
    if (o.mode === "precomp") {
        var nc = app.project.items.addComp("TVPaint Camera", comp.width, comp.height, comp.pixelAspect || 1, comp.duration, comp.frameRate);
        X = nc.layers.add(comp);
        var r = RK.cam.applyTvp(nc, X, cam, { anchor: o.anchor, autoParent: false });
        r.comp = nc;
        return r;
    }
    if (o.mode === "selected") {
        var sel = comp.selectedLayers;
        if (!sel.length) {
            RK.msg("No layer is selected. Select the layer to animate.");
            return null;
        }
        X = sel[0];
        // the Rotation & Scale layer finds X by name
        RK.nameCheck(comp);
    } else {
        X = comp.layers.addNull();
        X.name = RK.uniqueName(comp, "TVPaint Camera");
    }
    return RK.cam.applyTvp(comp, X, cam, o);
};

RK.cam.tvpState = { mode: 0, autoParent: true, anchor: 0, path: "" };

RK.cam.tvpPage = function (body) {
    var st = RK.cam.tvpState;
    var mode = RK.ui.radios(body, ["Use a Null Object", "Precompose layers", "Use selected layer"], st.mode, { vertical: true, spacing: 2 });
    var auto = RK.ui.check(body, "Auto-parent layers", st.autoParent, "Parent the comp's layers without a parent to the camera layer");
    var sync = function () { auto.enabled = mode.getValue() !== 1; };
    for (var i = 0; i < mode.radios.length; i++) {
        mode.radios[i].onClick = (function (f) { return function () { f.call(this); sync(); }; })(mode.radios[i].onClick);
    }
    sync();
    RK.ui.separator(body);
    var what = RK.ui.radios(body, ["Animate position", "Animate anchor point"], st.anchor, { vertical: true, spacing: 2 });
    RK.ui.separator(body, "Camera file (.cpt)");
    var path = RK.ui.text(body, st.path, RK.PAGE_W, "Paste the path of the camera file exported from TVPaint");
    RK.ui.bottomRow(body, [RK.ui.backItem("Cancel"), {
        label: "Import TVP Cam", icon: RK.I.ok, tip: "Imports a TVPaint camera",
        onClick: RK.ui.action("Import TVP Cam", function () {
            st.mode = mode.getValue();
            st.autoParent = auto.value;
            st.anchor = what.getValue();
            st.path = path.text;
            var comp = RK.comp();
            if (!comp) return false;
            var text = RK.cam.readFile(st.path);
            var r = null;
            RK.undo(RK.NAME + " - Import TVPaint Camera", function () {
                r = RK.cam.importTvp(comp, text, {
                    mode: st.mode === 1 ? "precomp" : st.mode === 2 ? "selected" : "null",
                    autoParent: st.autoParent && st.mode !== 1, anchor: st.anchor === 1
                });
            });
            return !!r;
        }, true)
    }]);
};

RK.tool("tvpaint", {
    label: "Import TVPaint Cam", icon: RK.cam.I.tvpaint,
    tip: "Imports a camera exported from TVPaint (position, zoom, rotation and timing).",
    page: function (body) { RK.cam.tvpPage(body); }
});

// ---------------------------------------------------------------------------- the tab

RK.ui.addTab("camera", "Cameras", RK.I.tabCam, "Cameras", function (page) {
    RK.ui.buildGrid(page, { left: ["controlcam", "zlink", "multiplane", "tvpaint"] });
});

// ============================================================================ Settings tab

RK.st = {};

// A titled frame (ScriptUI panel) holding a column of rows.
RK.st.box = function (parent, title) {
    var p = parent.add("panel", undefined, title);
    p.orientation = "column";
    p.alignChildren = ["left", "top"];
    p.alignment = ["fill", "top"];
    p.margins = [10, 18, 10, 8];
    p.spacing = 4;
    p.preferredSize = [RK.PAGE_W, -1];
    return p;
};

RK.st.LABEL_W = 78;

RK.st.general = function (g) {
    var ui = RK.st.box(g, "Interface");
    RK.ui.label(ui, "Tool box selector:");
    var useDrop = !!RK.pref("tabDropdown", false);
    RK.ui.radios(ui, ["Use buttons", "Use dropdown"], useDrop ? 1 : 0, {
        tips: ["The tool boxes are icons at the top right", "The tool boxes are a list at the top right"],
        onChange: function (i) {
            RK.setPref("tabDropdown", i === 1);
            RK.ui.setSelector(i === 1);
            RK.ui.relayout();
        }
    });
    RK.ui.separator(ui, "", RK.PAGE_W - 22);
    var ex = RK.ui.check(ui, "Expert mode (icons only)", RK.pref("expert", false), "Hides the labels of the tool buttons. Applies when the panel opens again.");
    ex.onClick = function () {
        RK.setPref("expert", this.value);
        RK.msg("Expert mode " + (this.value ? "on" : "off") + ": close and reopen the panel to apply it.");
    };
};

RK.st.rigging = function (g) {
    var b = RK.st.box(g, "Bones");
    var r1 = RK.ui.row(b, "Type:", RK.st.LABEL_W);
    var type = RK.ui.drop(r1, ["Solid", "Null"], RK.pref("boneType", "solid") === "null" ? 1 : 0, 0, "The layer each new bone is");
    var r2 = RK.ui.row(b, "Size:", RK.st.LABEL_W);
    var size = RK.ui.number(r2, RK.pref("boneSize", 20), 50, "Bone size in pixels (when Auto size is off)");
    RK.ui.label(r2, "px");
    var r3 = RK.ui.row(b, "", 0);
    var auto = RK.ui.check(r3, "Auto size", RK.pref("boneAutoSize", true), "Size the bones from the layer they are made for");
    var hints = ["small", "medium", "big"];
    var hint = RK.ui.drop(r3, ["Small", "Medium", "Big"], Math.max(0, hints.indexOf(RK.pref("boneSizeHint", "medium"))), 0, "Auto size");
    var r4 = RK.ui.row(b, "Color:", RK.st.LABEL_W);
    RK.ui.label(r4, "#");
    var color = RK.ui.text(r4, RK.pref("boneColor", "FF0000"), 70, "Colour of solid bones (hexadecimal RRGGBB)");
    var r5 = RK.ui.row(b, "Placement:", RK.st.LABEL_W);
    var places = ["top", "bottom", "over", "under"];
    var place = RK.ui.drop(r5, ["Top", "Bottom", "Over layer", "Under layer"], Math.max(0, places.indexOf(RK.pref("bonePlacement", "over"))), 0, "Where new bones go in the layer stack");
    var sync = function () {
        size.enabled = !auto.value;
        hint.enabled = auto.value;
        color.enabled = !type.selection || type.selection.index === 0;
    };
    type.onChange = function () { RK.setPref("boneType", this.selection && this.selection.index === 1 ? "null" : "solid"); sync(); };
    size.onChange = function () { RK.setPref("boneSize", Math.max(1, Math.round(RK.ui.num(size, 20, 1)))); };
    auto.onClick = function () { RK.setPref("boneAutoSize", this.value); sync(); };
    hint.onChange = function () { RK.setPref("boneSizeHint", hints[this.selection ? this.selection.index : 1]); };
    color.onChange = function () {
        var h = String(color.text).replace(/^#/, "");
        if (/^[0-9a-fA-F]{6}$/.test(h)) RK.setPref("boneColor", h.toUpperCase());
        else RK.msg("Type the bone colour as six hexadecimal digits, like FF0000.");
    };
    place.onChange = function () { RK.setPref("bonePlacement", places[this.selection ? this.selection.index : 2]); };
    sync();

    var c = RK.st.box(g, "Controllers");
    var c1 = RK.ui.row(c, "Type:", RK.st.LABEL_W);
    var ctype = RK.ui.drop(c1, ["Icon", "Null"], RK.pref("ctrlType", "icon") === "null" ? 1 : 0, 0,
        "Icon: a guide shape layer with a drawing. Null: a null object (the Controllers button then creates them at once).");
    ctype.onChange = function () { RK.setPref("ctrlType", this.selection && this.selection.index === 1 ? "null" : "icon"); };
    var c2 = RK.ui.row(c, "Placement:", RK.st.LABEL_W);
    var cplace = RK.ui.drop(c2, ["Top", "Bottom", "Over layer", "Under layer"], Math.max(0, places.indexOf(RK.pref("ctrlPlacement", "top"))), 0,
        "Where new controllers go in the layer stack, relative to the layer they are made for");
    cplace.onChange = function () { RK.setPref("ctrlPlacement", places[this.selection ? this.selection.index : 0]); };
    RK.ui.label(c, "Size and colour: in the Controllers tool.");
};

RK.st.animation = function (g) {
    var b = RK.st.box(g, "Interpolations");
    var inter = RK.ui.check(b, "Interactive update", RK.pref("interactiveUpdate", false),
        "Apply the influence while a slider is dragged. Each change is an undo step, so this can fill the whole undo history.");
    var buttons = !!RK.pref("influenceButtons", false);
    inter.enabled = !buttons;
    inter.onClick = function () {
        RK.setPref("interactiveUpdate", this.value);
        if (this.value) RK.msg("Interactive update is on: every change while dragging is an undo step and can fill the undo history.");
    };
    RK.ui.radios(b, ["Use sliders", "Use buttons"], buttons ? 1 : 0, {
        tips: ["The influence rows show a slider", "The influence rows show buttons: 10, 25, 50, 75, 90, 100"],
        onChange: function (i) {
            RK.setPref("influenceButtons", i === 1);
            inter.enabled = i === 0;
            RK.msg("Close and reopen the panel to switch between sliders and buttons.");
        }
    });
    var c = RK.st.box(g, "Copy / paste animation");
    var modes = ["names", "indexes", "selection"];
    RK.ui.radios(c, ["Use layer names", "Use layer indexes", "Use selection order"], Math.max(0, modes.indexOf(RK.pref("pasteMatch", "names"))), {
        vertical: true,
        tips: ["Paste on the layers with the same name", "Paste on the layers at the same index", "Paste on the selected layers, in selection order"],
        onChange: function (i) { RK.setPref("pasteMatch", modes[i]); }
    });
};

RK.ui.addTab("settings", "Settings", RK.I.tabSettings, "Settings", function (page) {
    page.spacing = 8;
    var sections = [["General", RK.st.general], ["Rigging", RK.st.rigging], ["Animation", RK.st.animation]];
    var names = [];
    for (var i = 0; i < sections.length; i++) names.push(sections[i][0]);
    var drop = RK.ui.drop(page, names, 0, 140, "Settings");
    drop.alignment = ["center", "top"];
    var stack = page.add("group");
    stack.orientation = "stack";
    stack.alignChildren = ["center", "top"];
    var groups = [];
    for (i = 0; i < sections.length; i++) {
        var g = stack.add("group");
        g.orientation = "column";
        g.alignChildren = ["fill", "top"];
        g.spacing = 8;
        g.visible = i === 0;
        groups.push({ g: g, build: sections[i][1], built: false });
    }
    var show = function (k) {
        for (var j = 0; j < groups.length; j++) {
            if (j === k && !groups[j].built) {
                groups[j].build(groups[j].g);
                groups[j].built = true;
            }
            groups[j].g.visible = j === k;
        }
        RK.ui.relayout();
    };
    drop.onChange = function () { show(this.selection ? this.selection.index : 0); };
    show(0);
});

// ============================================================================ start

// Test scripts set $.global.__RK_TEST before loading this file to get at RK.
if ($.global && $.global.__RK_TEST) $.global.__RK = RK;

RK.ui.build(thisObj);

})(this);
