This seemed like good practice so I've written one this morning: /**
* SetPath.jsx
*
* When the user selects a single anchorpoint of a single shape, this script will mimick a "Set First Vertex"
* menu action in After Effects along with adding "Reverse Path Direction" support either via prompt or config.
*/
var config = {
reverse: {
showPrompt: false, // If true, asks whether you want to reverse
value: false, // If true, will reverse paths. This is overridden whenever showPrompt above is set to true
title: "Reverse?", // Title of the window
message: "Choose yes if wanting to reverse path direction:", // Description of window
},
showAlert: true, // If true, displays an alert on completion
message: "Done!", // Contents of the alert on completion
//
// Various debugging utilities for catching errors and edgecases:
debug: {
alertSelectedAnchor: false, // if true, alert with original selected point index
reportNewAnchors: false, // if true, alert string of indices to rewrite as
endBeforeRunning: false, // if true, ends function before any modifications
},
};
//
var shouldReverse = false; // Needed as a global variable
Array.prototype.filter = function (callback) {
var filtered = [];
for (var i = 0; i < this.length; i++)
if (callback(this[i], i, this)) filtered.push(this[i]);
return filtered;
};
Array.prototype.setNewOrigin = function (index) {
if (index > this.length || index < 0) return this;
return [].concat(this.slice(index), this.slice(0, index));
};
Array.prototype.findIndex = function (callback) {
for (var i = 0; i < this.length; i++)
if (callback(this[i], i, this)) return i;
return -1;
};
Array.prototype.forEach = function (callback) {
for (var i = 0; i < this.length; i++) callback(this[i], i, this);
};
Array.prototype.map = function (callback) {
var mappedParam = [];
for (var i = 0; i < this.length; i++)
mappedParam.push(callback(this[i], i, this));
return mappedParam;
};
function get(type, parent) {
if (arguments.length == 1 || !parent) parent = app.activeDocument;
var result = [];
if (!parent[type]) return [];
for (var i = 0; i < parent[type].length; i++) result.push(parent[type][i]);
return result;
}
// Utility function that checks to ensure only one vertex is active and returns it if so
function hasSinglePointSelection(item) {
function findActiveVertices(item) {
return get("pathPoints", item)
.map(function (point, index) {
return {
index: index,
point: point,
};
})
.filter(function (item) {
return item.point.selected == PathPointSelection.ANCHORPOINT;
});
}
var selectedPoints = findActiveVertices(item);
if (!selectedPoints.length) return false;
else return selectedPoints.length == 1 ? selectedPoints[0] : false;
}
function main() {
// First grab selection
var selection = get("selection");
// But ensure only one object is selected:
if (!selection.length || selection.length > 1)
return alert("Must have a selection and only one path selected");
// Isolate our target shape and ensure it's the correct type:
var target = selection[0];
if (!/path/i.test(target.typename) || /compound/i.test(target.typename))
return alert("Selection must be a valid PathItem and not a compound path");
// Ensure only one anchor has been selected so we know our intended origin:
var chosenVertex = hasSinglePointSelection(target);
if (!chosenVertex) return alert("Must have only one vertex selected");
var newOriginIndex = chosenVertex.index;
if (config.debug.alertSelectedAnchor) alert(newOriginIndex);
// Since PathPoint is a ref and mutable, we need to shallow copy it to rearrange the entire set:
var points = get("pathPoints", target)
.map(function (point, index) {
return {
index: index,
data: shallowCopy(point),
};
})
// And use a shorthand utility for setting the new index as the first vertex:
.setNewOrigin(newOriginIndex);
// Very basic UI to flip path direction via confirm dialog:
shouldReverse = config.reverse.value;
if (config.reverse.showPrompt)
shouldReverse = confirm(
config.reverse.message,
!config.reverse.value,
config.reverse.title
);
if (shouldReverse) {
// We can't simply reverse an array like [0,1,2,3] if 0 is our selection because then it becomes [3,2,1,0]:
points = points.reverse();
// We have to set the new origin of the reversed array back to the new origin, like [0,3,2,1]
points = points.setNewOrigin(
points.findIndex(function (point) {
return point.index == newOriginIndex; // So we use an ES6-like array method of findIndex
})
);
}
// This just reports a list of anchors like [2,3,0,1] in case anything weird is happening:
if (config.debug.reportNewAnchors) {
var str = "";
str += points
.map(function (point) {
return point.index;
})
.join(",");
alert(str);
}
if (config.debug.endBeforeRunning) return null;
// So for each point, we rewrite it with shallow copy values:
get("pathPoints", target).forEach(function (point, index) {
rewritePoint(point, points[index].data, index);
});
// Then we have to manually clear the selection, because for some reason AI deselects the entire object if you toggle point selections too much:
get("pathPoints", target).forEach(function (point, index) {
point.selected = PathPointSelection.NOSELECTION;
});
// And manually reselect the first vertex, which should correspond to the original selected vertex we'd wanted to set as First Vertex before running the script
target.pathPoints[0].selected = PathPointSelection.ANCHORPOINT;
if (config.showAlert) alert(config.message);
}
function shallowCopy(ref) {
var temp = {};
for (var key in ref) temp[key] = ref[key];
return temp;
}
function rewritePoint(a, b, index) {
var keys = ["anchor", "leftDirection", "rightDirection"];
keys.forEach(function (key) {
var tempKey = key + "";
// If we reverse directions, then we have to set leftDirection > rightDirection and vice versa otherwise we produce artifacts:
if (/direction/i.test(key) && shouldReverse && b[key])
tempKey =
key.replace("Direction", "") == "right"
? "leftDirection"
: "rightDirection";
a[tempKey] = b[key];
});
}
main(); Unlike the one shown above, this works based on user selection. If you select an anchor of a shape then run this script it will mimick a Set First Vertex action of After Effects without creating new annotation text or requiring you to know the point index in order to type it into a prompt, reversing the path is optional and can be set to default (or set to prompt you) in the config variable at the top:
... View more