Copy link to clipboard
Copied
Hello,
I am attempting to solve a specific task required for my Adobe Illustrator + After Effects Masks workflow. I am familiar with various manual and automated techniques for working with shapes and masks, but in this particular case, I want to address this task in its specific form.
Essentially, my goal is to develop a tool (script/plugin) that manipulates a Compound Path in a project similar to how I might use the Knife tool to transform a Compound Path into a continuous solid Path. Ultimately, I want to produce a single, solid mask in an After Effects project from this. I have attached a screenshot for clarity.
(I've also attached the image as a file to the post so it can be seen in more detail)
I found that the Adobe Illustrator API does not provide a straightforward way to automate the use of the Knife tool. However, through trial and error, I discovered that drawing thin shapes (0.01pt thick) at the intersections of the inner and outer paths of the Compound Path and then applying Pathfinder > Divide achieves the desired result, which can be automated. This led to the creation of the script shown below, but it is only a starting point and operates quite crudely by just creating intersections with thin lines on each Compound Path and applying Pathfinder. This results in a non-compound Path, but not as a single continuous path, rather it's segmented into multiple parts.
I would like to more elegantly solve this problem. Logically, the code needs to first identify random spots on the Compound Path where cuts along the internal and external paths can be made, to then implement logic in the script to place lines of 0.01pt thickness precisely at these spots. However, I have no idea what specific information I need for this, and extensive online research has turned up nothing, as this task is quite specific.
I would appreciate any help on this issue, and any suggestions on what information I should look for.
Thank you!
The example code I wrote above:
function drawThinRectangle(centerX, centerY, width, height, angle) {
var rect = app.activeDocument.pathItems.rectangle(centerY + height / 2, centerX - width / 2, width, height);
rect.stroked = false;
rect.filled = true;
rect.fillColor = app.activeDocument.swatches[0].color;
rect.rotate(angle);
return rect;
}
function applyPathfinderToGroup(groupItem) {
// Selects all page items inside the group
for (var i = 0; i < groupItem.pageItems.length; i++) {
groupItem.pageItems[i].selected = true;
}
// Apply Pathfinder Subtract
app.executeMenuCommand('Live Pathfinder Subtract');
app.executeMenuCommand('expandStyle');
// Ungroup the expanded pathfinder result
app.executeMenuCommand('ungroup');
}
function processCompoundPath(compoundPathItem, index) {
var doc = app.activeDocument;
var layer = compoundPathItem.layer;
var groupItems = layer.groupItems.add();
groupItems.name = "Comp_" + ("0" + (index + 1)).slice(-2);
var bounds = compoundPathItem.geometricBounds; // [x1, y1, x2, y2]
var centerX = (bounds[0] + bounds[2]) / 2;
var centerY = (bounds[1] + bounds[3]) / 2;
var layerWidth = bounds[2] - bounds[0];
var layerHeight = bounds[1] - bounds[3];
var maxDimension = Math.max(layerWidth, layerHeight);
// Create “lines” as thin rectangles for the current Compound Path
var thinRectVertical = drawThinRectangle(centerX, centerY, 0.001, layerHeight, 0);
var thinRectHorizontal = drawThinRectangle(centerX, centerY, layerWidth, 0.001, 0);
var thinRectDiagonal1 = drawThinRectangle(centerX, centerY, 0.001, maxDimension, 45);
var thinRectDiagonal2 = drawThinRectangle(centerX, centerY, 0.001, maxDimension, -45);
// Move compound path and lines to a new group
compoundPathItem.moveToBeginning(groupItems);
thinRectVertical.moveToBeginning(groupItems);
thinRectHorizontal.moveToBeginning(groupItems);
thinRectDiagonal1.moveToBeginning(groupItems);
thinRectDiagonal2.moveToBeginning(groupItems);
applyPathfinderToGroup(groupItems);
}
function main() {
var doc = app.activeDocument;
var allCompoundPaths = [];
for (var i = 0; i < doc.pageItems.length; i++) {
if (doc.pageItems[i].typename === 'CompoundPathItem') {
allCompoundPaths.push(doc.pageItems[i]);
}
}
if (allCompoundPaths.length === 0) {
alert('There are no compound paths in the document.');
return;
}
for (var j = 0; j < allCompoundPaths.length; j++) {
processCompoundPath(allCompoundPaths[j], j);
}
app.activeDocument.selection = null;
}
main();
Hello,
Today I was testing different scripts for processing the shapes you provided as an example and was able to create another script that is more suitable for processing strict monolithic shapes with gaps.
The previous script, on the other hand, works best with freeform shapes.
P.S. I had to remove one part on one of the shapes, because in any of the scripts, if you make a cut there, that part will be a separate path, and the goal was to create solid paths from complex shapes.
P.S.S. I removed th
...Copy link to clipboard
Copied
In case you mainly have rather simple compound paths as shown in your screenshot, it can be done with an action.
Would an action even be an option for you?
Or are there compound paths which are way more complicated?
Copy link to clipboard
Copied
If you mean doing it manually then you are right, it is possible, however when there are many such forms it can be difficult and I was looking for an automation solution.
However, today I came up with a solution after posting this thread. I couldn't come up with it for a long time, but I did solve this problem and I'm incredibly happy about it.
I'll leave the code for the solution here and hopefully it will help someone someday, if someone is looking for the same thing I was looking for:
function collectCompoundPaths(item, paths) {
if (item.typename === 'CompoundPathItem') {
paths.push(item);
} else if (item.typename === 'GroupItem' || item.typename === 'Layer') {
for (var i = 0; i < item.pageItems.length; i++) {
collectCompoundPaths(item.pageItems[i], paths);
}
if (item.typename === 'Layer') {
for (var j = 0; j < item.layers.length; j++) {
collectCompoundPaths(item.layers[j], paths);
}
}
}
}
function main() {
var doc = app.activeDocument;
var allCompoundPaths = [];
for (var i = 0; i < doc.layers.length; i++) {
collectCompoundPaths(doc.layers[i], allCompoundPaths);
}
for (var j = 0; j < allCompoundPaths.length; j++) {
findAndDrawRectangles(allCompoundPaths[j]);
}
}
function findAndDrawRectangles(compoundPathItem) {
function distance(point1, point2) {
return Math.sqrt(Math.pow(point2[0] - point1[0], 2) + Math.pow(point2[1] - point1[1], 2));
}
function drawThinRectangle(x1, y1, x2, y2, width) {
var dx = x2 - x1;
var dy = y2 - y1;
var angle = Math.atan2(dy, dx) * (180 / Math.PI);
var length = Math.sqrt(dx * dx + dy * dy);
var centerX = (x1 + x2) / 2;
var centerY = (y1 + y2) / 2;
var extraLength = length * 0.00;
centerX -= Math.cos(angle * Math.PI / 180) * extraLength;
centerY -= Math.sin(angle * Math.PI / 180) * extraLength;
var rect = app.activeDocument.pathItems.rectangle(centerY + width / 2, centerX - length / 2, length, width);
rect.rotate(angle);
rect.stroked = false;
rect.filled = true;
rect.fillColor = app.activeDocument.swatches[0].color;
return rect;
}
var doc = app.activeDocument;
doc.selection = null;
compoundPathItem.selected = true;
app.executeMenuCommand('noCompoundPath');
var paths = app.activeDocument.selection;
var maxArea = 0, mainPath, rectangles = [];
// Находим самый большой путь
for (var i = 0; i < paths.length; i++) {
var bounds = paths[i].geometricBounds;
var area = (bounds[2] - bounds[0]) * (bounds[1] - bounds[3]);
if (area > maxArea) {
maxArea = area;
mainPath = paths[i];
}
}
if (!mainPath) {
alert("No valid main path found.");
return;
}
for (var j = 0; j < paths.length; j++) {
if (paths[j] === mainPath) continue;
var minDistance = Infinity;
var closestPointInner, closestPointOuter;
for (var k = 0; k < paths[j].pathPoints.length; k++) {
var innerPoint = paths[j].pathPoints[k].anchor;
for (var l = 0; l < mainPath.pathPoints.length; l++) {
var outerPoint = mainPath.pathPoints[l].anchor;
var dist = distance(innerPoint, outerPoint);
if (dist < minDistance) {
minDistance = dist;
closestPointInner = innerPoint;
closestPointOuter = outerPoint;
}
}
}
rectangles.push(drawThinRectangle(closestPointInner[0], closestPointInner[1], closestPointOuter[0], closestPointOuter[1], 0.01));
}
for (var m = 0; m < rectangles.length; m++) {
rectangles[m].selected = true;
}
app.executeMenuCommand('group');
app.executeMenuCommand('Live Pathfinder Subtract');
app.executeMenuCommand('expandStyle');
app.executeMenuCommand('ungroup');
doc.selection = null;
}
main();
Copy link to clipboard
Copied
I didn't mean doing it manually. I meant an automated way with an Illustrator action.
It would work with rather simple compound paths, but not very well with complicated compound constructions.
As far as I can see, the same applies to your scripting approach. It works well with simple compound paths as shown in your initial post, but it may often fail or at least do some funny things when processing complex compound paths.
PS: MotionThunder deleted two posts after I answered. Therefore it may sound a bit confusing.
Copy link to clipboard
Copied
No, this new solution is designed to work with any contours, including those of any complexity.
For example, it now easily processes multi-jointed Compound with a large number of holes and any shape. And this is exactly what I needed - correct work with any shape.
Copy link to clipboard
Copied
Well, that certainly depends on what you mean by "any complexity".
You may want to download the sample file below. It contains four compound paths. I'd not call them overly complex. Try to run your script in that file and see what happens.
Please don't get me wrong. I think your script is very good (thanks for sharing), but one may keep in mind that it will not always work as intended.
Copy link to clipboard
Copied
Yeah, I can see that. In this case, the problems are mostly due to the small number of points. It would probably be accurate to say that this script is not very suitable in the case of smooth and precise shapes, precisely because it builds its cuts by analyzing the closest points on the inner and outer paths of the shape.
I think advanced programmers could write this solution better, but in my usage scenario, where mostly shapes are created with brushes, this tool fits well.
Thanks for your tests though, I can see these problems now.
Copy link to clipboard
Copied
Hello,
Today I was testing different scripts for processing the shapes you provided as an example and was able to create another script that is more suitable for processing strict monolithic shapes with gaps.
The previous script, on the other hand, works best with freeform shapes.
P.S. I had to remove one part on one of the shapes, because in any of the scripts, if you make a cut there, that part will be a separate path, and the goal was to create solid paths from complex shapes.
P.S.S. I removed the big rectangle with holes because Adobe software takes too long to process it and I couldn't record video.
I will attach to this post the code for two script variants that work differently with different forms, and each can be applied in a certain situation where the other fails. Maybe someday this post will be useful to someone.
Copy link to clipboard
Copied
del
Copy link to clipboard
Copied
Great improvements, MotionThunder. Thanks for sharing it.
I'm going to do some further tests tomorrow and may report back.
Copy link to clipboard
Copied
del
Copy link to clipboard
Copied
del