Copy link to clipboard
Copied
How many times have you been writing a function or a loop that attempts to touch all elements of a collection, like swatches or layers or groupItems, only to find that when the code finished, only approximately half of the elements had been processed?? If it has happened to you as often as it has happened to me, you'll quickly be able to identify the issue and apply the appropriate facepalm to your forehead. But what a headache it is to get to that point of easy recognition and finally break bad habits and start spending more mental energy on "what direction should this loop go? will i be deleting or moving or adding anything?
What if we didn't have to worry about that? What if we had access to tools more like modern javascript that can remove the tedium and the need for spending mental energy on things that don't matter to what we want to accomplish so that we can stay focused on the bigger picture?
By adding one line of code (or simply prepending one short function call to some array prototype function like forEach() or filter()) you can forget about whether or not your loop will be affecting the index of the items you want to manipulate. Additionally, creating a standard array out of a collection makes it easier for you to dictate the order in which things are processed as well as dictate the stacking order of the processed art. Collections give you access to the zOrder() method, but this can get pretty cumbersome if you're not just trying to put something at the top or bottom or move it up/down once. With a standard array, you can sort it using your own callback function to dictate the sorting behavior, and then it will be guaranteed that specific art will process when and finish where you want it, without ever having an indexing issue as a result of collections' "collapsing" and "expanding" behavior when things are added/deleted/moved.
All you need is this:
afc(app.activeDocument,"layers"); //if you prefer a more verbose function name, you can use arrayFromContainer()
//or
afc(myLayer,"pageItems");
Then this can easily be follwed by an array prototype function(s)
afc(myLayer,"pageItems").forEach(function(item){item.moveToBeginning(destLayer)});
or, let's say you wanted all the textFrames on myLayer whose name matches a certain regex?
afc(myLayer,"textFrames").filter(function(frame){return frame.name.match(/regex/i)});
then of course you could tack a forEach() onto the end of that or a map() or whatever you want.
Say you have a big messy artboard with lots of disconnected/disjointed stuff on it and you want to just sort it out so it's easier to see and manage.. let's sort them all by size, then place them in a neat grid, then move any large items into a "large items" layer and same treatment for small items. you can easily do all of this in just a couple minutes of coding if you're not bogged down with too much minutiae. like this:
var doc = app.activeDocument;
var bigLayer = doc.layers.add();
bigLayer.name = "big shapes";
var smallLayer = doc.layers.add();
smallLayer.name = "small shapes";
var curPos = [0, 0]
var spacing = 5;
afc(layers[2], "pathItems").sort(function (a, b) {
return a.area > b.area
}).forEach(function (path) {
if(path.width >= 15 && path.width <= 35)
{
//delete any item between 15 and 35 pt wide
path.remove();
return;
}
//send the path to the back of the layer to maintain the sort order of the paths
path.moveToEnd(path.width < 15 ? smallLayer : bigLayer);
path.position = curPos;
curPos[0] += path.width + spacing;
if (curPos[0] > doc.width - path.width) {
curPos[0] = 0;
curPos[1] -= path.height + spacing;
}
});
before:
after:
It may not seem like much, but it has absolutely changed the way I code for illustrator (and improved the cleanliness of the code) and it has drastically improved my ability to produce very rapid prototypes to test out ideas and concepts without wasting time coding for edge cases and trying to handle possible silly errors. Maybe you will also find it useful. Here's the github link and the source code:
https://github.com/wdjsdev/public_illustrator_scripts/blob/master/array_from_container.js
**Updated 22 August, 2022**
added support for passing in an array of parent containers and childTypes. A use case example would be "i need all the textFrames and pathItems on layers[0] and layers[1]". You can pass in an array for both arguments to get back a conglomerate array of multiple item types from multiple parents, like so:
var myItems = afc([layers[0],layers[1]],["pathItems","textFrames"]);
Also updated how default values are handled and improved error handling for container/childType mismatches.
And on the github page, there is a comprehensive test function at the bottom.
function afc ( containers, childTypes )
{
const CHILD_TYPE_OPTIONS = [ "pageItems", "layers", "pathItems", "compoundPathItems", "groupItems", "swatches", "textFonts", "textFrames", "placedItems", "rasterItems", "symbolItems", "pluginItems", "artboards", "selection", "characterStyles", "paragraphStyles", "brushes" ];
const DEFAULTS = { "app": "documents", "SwatchGroup": "swatches", "Document": "layers", "GroupItem": "pageItems", "CompoundPathItem": "pathItems", "TextFrame": "textRanges", "Selection": "pageItems", "all": "all", undefined: "pageItems" };
var result = [];
if ( !containers.toString().match( /array/i ) )
{
containers = [ containers ];
}
if ( !childTypes.toString().match( /array/i ) )
{
childTypes = childTypes || DEFAULTS[ ctn ] || ( function () { $.writeln( "No childTypes given. Defaulting to pageItems" ); return "pageItems"; } )();
childTypes = [ childTypes ];
}
function loopArray ( array, callback )
{
for ( var i = 0; i < array.length; i++ )
{
callback( array[ i ], i );
}
}
loopArray( containers, function ( curContainer )
{
//validate containers
var ctn = curContainer.typename;
if ( !ctn.match( /app|adobe|document|layer|group|compound|text/i ) )
{
$.writeln( "ERROR: afc(" + containers.name + "," + childTypes + ");" );
$.writeln( "Can't make array from this containers. Invalid containers type: " + containers.typename );
return;
}
loopArray( childTypes, function ( curChildType )
{
if ( !curContainer[ curChildType ] )
{
$.writeln( "ERROR: afc(" + containers.name + "," + childTypes + ");" );
$.writeln( "Can't make array of " + curChildType + " from this container." );
return;
}
for ( var i = 0; i < curContainer[ curChildType ].length; i++ )
{
result.push( curContainer[ curChildType ][ i ] );
}
} );
} )
return result;
}
Copy link to clipboard
Copied
I was thinking about that, too. Tom Scharstein (Inventsable) converts Collections to clear Arrays in his scripts. If I have a lot of "for" loops for arrays, I try to add a prototype function forEach() to my code.
Copy link to clipboard
Copied
I can't really think of any reason not to convert to array first. The collection is still always available if you need a "snapshot" of all the items/elements at a specific time.. So at any time you can replace your array with fresh updated elements from the array. It's super easy to make the array and it can prevent lots of issues and save lots of typing if array prototypes are used. The scope enhancements are my favorite part. I've long lamented that extendscript doesn't support "let" which helps solve a bunch of scoping issues.. making an array and using callback functions mimics the behavior of "let" from modern javascript by keeping any variables created contained within the callback function scope. So, after your array prototype callback is finished executing, any variables used are discarded and won't interfere with anything else in your code. This can help avoid having many different similarly named variables and make it much easier to track what your code is doing at a given time because variable names can be simple, but their context can still be easily determined with a terse comment before the array prototype function call. I know what's inside the array I'm processing, so i can get away with using "item" as a simple variable inside the callback function without fear that later on i'll be wondering "wait.. wtf is item?! why didn't i use a super_long_very_descriptive_variable_name?! It's always clear that "item" just refers to the current element of the array. So all i need to ask is "what's inside the array on which this function is being called?" and then i know exactly what "item" is. Then the code is cleaner and more consistent because no matter what the callback function is, i'm using the same simple scheme for everything which makes it easy to debug.
I include all of these array prototypes along with my utilities that are used in every script and they make rapid development so much quicker and easier:
Array.prototype.indexOf = function(a, b, c){
for (c = this.length, b = (c + ~~b) % c; b < c && (!(b in this) || this[b] !== a); b++);
return b ^ c ? b : -1;
}
Array.prototype.map = function(callback) {
arr = [];
for (var i = 0; i < this.length; i++)
arr.push(callback(this[i], i, this));
return arr;
};
Array.prototype.forEach = function(callback,startPos,inc) {
inc = inc || 1;
startPos = startPos || 0;
for (var i = startPos; i < this.length; i+=inc)
callback(this[i], i, this);
};
Array.prototype.backForEach = function(callback,startPos,inc) {
inc = inc || 1;
startPos = startPos || this.length-1;
for (var i = startPos; i >= 0; i--)
callback(this[i], i, this);
};
Array.prototype.filter = function(callback, context) {
arr = [];
for (var i = 0; i < this.length; i++) {
if (callback.call(context, this[i], i, this))
arr.push(this[i]);
}
return arr;
};
Array.prototype.reverse = function()
{
var arr = [];
for(var i=this.length-1;i>=0;i--)
{
arr.push(this[i]);
}
return arr;
}
Copy link to clipboard
Copied
arr.reverse();
Copy link to clipboard
Copied
Yea that's very possible. I know I tested a few of them and they didn't work, so I found a set online and shamelessly swiped the whole thing. Then I updated a few of them. Mainly I added a startPos and increment argument to forEach so that you can do things like "start at index 4 and loop every other item" if there were some silly reason that became necessary.
Then I also duplicated forEach and made it run backwards.. Though now I'm realizing that reverse().forEach would do that just fine.
Copy link to clipboard
Copied
Other useful ones are •find, •findIndex, •some and •every. Although extendscript has no 'let', using TypeScript will allow one to use it and the modern coding conventions such as the arrow functions. Besides the modern syntax, TS helps with type checking, so for example if you use the .find method on an array of PathItem, the 'm' variable in the find parameter will automatically know it is a PathItem and VSCode will auto-complete all the various PathItem properties when you type "m.".
Copy link to clipboard
Copied
Thanks for sharing.
Copy link to clipboard
Copied
awesome! thanks for sharing William!
Copy link to clipboard
Copied
Nice work!
Copy link to clipboard
Copied
Bumping to the top for exposure of updates.
I added support for passing in arrays of container objects and childTypes. This way you could get all of the textFrames, pathItems, and linkedItems from layers["myLayer1"], layers["myOtherLayer"], and layers["myParentLayer"].layers["mySubLayer"].
Perhaps part of your process is to locate and delete (or otherwise manipulate) a certain set of items. You could easily obtain an array of all relevant items, then .filter() the array to identify the desired items by name or other properties.
Also improved error handling and how defaults are handled.