Copy link to clipboard
Copied
Hello Community, I need help with an Adobe Illustrator script for Stacked Object Distribution. Simple objects work fine, but when they are objects with a clipping mask, the same does not happen. Objects with a mask are stacked according to the content they have inside in order to align with the simple objects. Let me explain in the sample image.
Copy link to clipboard
Copied
here the script.
var doc = app.activeDocument;
var selx = doc.selection;
if (selx.length < 2) {
alert("Por favor, selecciona al menos 2 objetos.");
} else {
var userValues = showDialog();
if (userValues) {
distributeObjects(selx, userValues);
}
}
function showDialog() {
var dialog = new Window('dialog', 'Distribuir Objetos Apilados');
dialog.add('statictext', undefined, 'Separación:');
var separationInput = dialog.add('edittext', undefined, '1');
separationInput.characters = 5;
var unitsGroup = dialog.add('group');
var cmButton = unitsGroup.add('radiobutton', undefined, 'cm');
var mmButton = unitsGroup.add('radiobutton', undefined, 'mm');
var pxButton = unitsGroup.add('radiobutton', undefined, 'px');
cmButton.value = true;
dialog.add('statictext', undefined, 'Modo de distribución:');
var modeGroup = dialog.add('group');
var gridButton = modeGroup.add('radiobutton', undefined, 'Grid');
var horizontalButton = modeGroup.add('radiobutton', undefined, 'Horizontal');
var verticalButton = modeGroup.add('radiobutton', undefined, 'Vertical');
verticalButton.value = true;
dialog.add('statictext', undefined, 'Columnas (solo para Grid):');
var columnsInput = dialog.add('edittext', undefined, '3');
columnsInput.characters = 5;
var buttons = dialog.add('group');
buttons.alignment = 'center';
var cancelButton = buttons.add('button', undefined, 'Cancelar', { name: 'cancel' });
var okButton = buttons.add('button', undefined, 'Aceptar', { name: 'ok' });
okButton.onClick = function () {
dialog.close(1);
};
cancelButton.onClick = function () {
dialog.close(0);
};
if (dialog.show() == 1) {
return {
separation: parseFloat(separationInput.text),
unit: cmButton.value ? 'cm' : (mmButton.value ? 'mm' : 'px'),
mode: gridButton.value ? 'grid' : (horizontalButton.value ? 'horizontal' : 'vertical'),
columns: parseInt(columnsInput.text)
};
}
return null;
}
function distributeObjects(sel, userValues) {
var separation = userValues.separation;
var unitMultiplier = (userValues.unit == 'cm') ? 28.3465 : (userValues.unit == 'mm' ? 2.83465 : 1);
separation *= unitMultiplier;
var mode = userValues.mode;
var gridCols = (mode == 'grid') ? userValues.columns : (mode == 'horizontal' ? sel.length : 1);
var currentX = 0, currentY = 0, maxRowH = 0;
if (mode == 'horizontal' || mode == 'grid') {
sel.sort(function (a, b) {
return getVisibleBounds(a)[0] - getVisibleBounds(b)[0];
});
} else if (mode == 'vertical') {
sel.sort(function (a, b) {
return getVisibleBounds(a)[3] - getVisibleBounds(b)[3];
});
}
for (var i = 0; i < sel.length; i++) {
var bounds = getVisibleBounds(sel[i]);
var objectWidth = bounds[2] - bounds[0];
var objectHeight = bounds[1] - bounds[3];
var posX = currentX;
var posY = currentY;
sel[i].position = [posX, posY];
if (mode == 'horizontal') {
currentX += (objectWidth + separation);
} else if (mode == 'vertical') {
currentY -= (objectHeight + separation);
} else if (mode == 'grid') {
currentX += (objectWidth + separation);
if ((i % gridCols) == (gridCols - 1)) {
currentX = 0;
currentY -= (maxRowH + separation);
maxRowH = 0;
}
maxRowH = Math.max(maxRowH, objectHeight);
}
}
}
function getVisibleBounds(item) {
if (item.typename === 'GroupItem' && item.clipped) {
for (var i = 0; i < item.pageItems.length; i++) {
if (item.pageItems[i].clipping) {
return item.pageItems[i].geometricBounds;
}
}
}
return item.geometricBounds;
}
Copy link to clipboard
Copied
Hi @Utson_Avila2888 for your learning, I've made two changes to your code:
1. I've written a function—getItemBoundsIllustrator—that does a bit more heavy lifting when trying to get the actual bounds, and I've called it from your distributeObjects function, and
2. I've changed the way the items are positioned: rather than settings "position" property, I use the translate method—you can do it either way, but I find translate more understandable.
I tested with your sample file and it seemed to work correctly for me. Let me know if that gets you moving forward.
- Mark
(function () {
var doc = app.activeDocument;
var selx = doc.selection;
if (selx.length < 2) {
alert("Por favor, selecciona al menos 2 objetos.");
} else {
var userValues = showDialog();
if (userValues) {
distributeObjects(selx, userValues);
}
}
})();
function showDialog() {
var dialog = new Window('dialog', 'Distribuir Objetos Apilados');
dialog.add('statictext', undefined, 'Separación:');
var separationInput = dialog.add('edittext', undefined, '1');
separationInput.characters = 5;
var unitsGroup = dialog.add('group');
var cmButton = unitsGroup.add('radiobutton', undefined, 'cm');
var mmButton = unitsGroup.add('radiobutton', undefined, 'mm');
var pxButton = unitsGroup.add('radiobutton', undefined, 'px');
cmButton.value = true;
dialog.add('statictext', undefined, 'Modo de distribución:');
var modeGroup = dialog.add('group');
var gridButton = modeGroup.add('radiobutton', undefined, 'Grid');
var horizontalButton = modeGroup.add('radiobutton', undefined, 'Horizontal');
var verticalButton = modeGroup.add('radiobutton', undefined, 'Vertical');
verticalButton.value = true;
dialog.add('statictext', undefined, 'Columnas (solo para Grid):');
var columnsInput = dialog.add('edittext', undefined, '3');
columnsInput.characters = 5;
var buttons = dialog.add('group');
buttons.alignment = 'center';
var cancelButton = buttons.add('button', undefined, 'Cancelar', { name: 'cancel' });
var okButton = buttons.add('button', undefined, 'Aceptar', { name: 'ok' });
okButton.onClick = function () {
dialog.close(1);
};
cancelButton.onClick = function () {
dialog.close(0);
};
if (dialog.show() == 1) {
return {
separation: parseFloat(separationInput.text),
unit: cmButton.value ? 'cm' : (mmButton.value ? 'mm' : 'px'),
mode: gridButton.value ? 'grid' : (horizontalButton.value ? 'horizontal' : 'vertical'),
columns: parseInt(columnsInput.text)
};
}
return null;
}
function distributeObjects(sel, userValues) {
var separation = userValues.separation;
var unitMultiplier = (userValues.unit == 'cm') ? 28.3465 : (userValues.unit == 'mm' ? 2.83465 : 1);
separation *= unitMultiplier;
var mode = userValues.mode;
var gridCols = (mode == 'grid') ? userValues.columns : (mode == 'horizontal' ? sel.length : 1);
var currentX = 0, currentY = 0, maxRowH = 0;
if (mode == 'horizontal' || mode == 'grid') {
sel.sort(function (a, b) {
return getVisibleBounds(a)[0] - getVisibleBounds(b)[0];
});
} else if (mode == 'vertical') {
sel.sort(function (a, b) {
return getVisibleBounds(a)[3] - getVisibleBounds(b)[3];
});
}
for (var i = 0; i < sel.length; i++) {
var bounds = getItemBoundsIllustrator(sel[i]);
var objectWidth = bounds[2] - bounds[0];
var objectHeight = bounds[1] - bounds[3];
var dx = currentX - bounds[0];
var dy = currentY - bounds[1];
sel[i].translate(dx, dy, true, true, true, true);
if (mode == 'horizontal') {
currentX += (objectWidth + separation);
} else if (mode == 'vertical') {
currentY -= (objectHeight + separation);
} else if (mode == 'grid') {
currentX += (objectWidth + separation);
if ((i % gridCols) == (gridCols - 1)) {
currentX = 0;
currentY -= (maxRowH + separation);
maxRowH = 0;
}
maxRowH = Math.max(maxRowH, objectHeight);
}
}
}
function getVisibleBounds(item) {
if (item.typename === 'GroupItem' && item.clipped) {
for (var i = 0; i < item.pageItems.length; i++) {
if (item.pageItems[i].clipping) {
return item.pageItems[i].geometricBounds;
}
}
}
return item.geometricBounds;
}
/**
* Returns bounds of item(s).
* @author m1b
* @version 2025-02-25
* @param {PageItem|Array<PageItem>} item - an Illustrator PageItem or array of PageItems.
* @param {Boolean} [geometric] - if false, returns visible bounds.
* @param {Array} [bounds] - private parameter, used when recursing.
* @returns {Array} - the calculated bounds.
*/
function getItemBoundsIllustrator(item, geometric, bounds) {
var newBounds = [],
boundsKey = geometric ? 'geometricBounds' : 'visibleBounds';
if (undefined == item)
return;
if (
item.typename == 'GroupItem'
|| item.constructor.name == 'Array'
) {
var children = item.typename == 'GroupItem' ? item.pageItems : item,
contentBounds = [],
isClippingGroup = (item.hasOwnProperty('clipped') && item.clipped == true),
clipBounds;
for (var i = 0, child; i < children.length; i++) {
child = children[i];
if (
child.hasOwnProperty('clipping')
&& true === child.clipping
&& true !== child.stroked
&& true !== child.filled
)
// the clipping item
clipBounds = child.geometricBounds;
else
contentBounds.push(getItemBoundsIllustrator(child, geometric, bounds));
}
newBounds = combineBounds(contentBounds);
if (
isClippingGroup
&& clipBounds
)
newBounds = intersectionOfBounds([clipBounds, newBounds]);
}
else if (
'TextFrame' === item.constructor.name
&& TextType.AREATEXT !== item.kind
) {
// get bounds of outlined text
var dup = item.duplicate().createOutline();
newBounds = dup[boundsKey];
dup.remove();
}
else if (item.hasOwnProperty(boundsKey)) {
newBounds = item[boundsKey];
}
// `bounds` will exist if this is a recursive execution
bounds = (undefined == bounds)
? bounds = newBounds
: bounds = combineBounds([newBounds, bounds]);
return bounds;
};
/**
* Returns the combined bounds of all bounds supplied.
* Works with Illustrator or Indesign bounds.
* @author m1b
* @version 2024-03-09
* @param {Array<bounds>} boundsArray - an array of bounds [L, T, R, B] or [T, L , B, R].
* @returns {bounds?} - the combined bounds.
*/
function combineBounds(boundsArray) {
var combinedBounds = boundsArray[0],
comparator;
if (/indesign/i.test(app.name))
comparator = [Math.min, Math.min, Math.max, Math.max];
else
comparator = [Math.min, Math.max, Math.max, Math.min];
// iterate through the rest of the bounds
for (var i = 1; i < boundsArray.length; i++) {
var bounds = boundsArray[i];
combinedBounds = [
comparator[0](combinedBounds[0], bounds[0]),
comparator[1](combinedBounds[1], bounds[1]),
comparator[2](combinedBounds[2], bounds[2]),
comparator[3](combinedBounds[3], bounds[3]),
];
}
return combinedBounds;
};
/**
* Returns the overlapping rectangle
* of two or more rectangles.
* NOTE: Returns undefined if ANY
* rectangles do not intersect.
* @author m1b
* @version 2024-09-05
* @param {Array<bounds>} arrayOfBounds - an array of bounds [L, T, R, B] or [T, L , B, R].
* @returns {bounds?} - intersecting bounds.
*/
function intersectionOfBounds(arrayOfBounds) {
var comparator;
if (/indesign/i.test(app.name))
comparator = [Math.max, Math.max, Math.min, Math.min];
else
comparator = [Math.max, Math.min, Math.min, Math.max];
// sort a copy of array
var bounds = arrayOfBounds
.slice(0)
.sort(function (a, b) { return b[0] - a[0] || a[1] - b[1] });
// start with first bounds
var intersection = bounds.shift(),
b;
// compare each bounds, getting smaller
while (b = bounds.shift()) {
// if doesn't intersect, bail out
if (!boundsDoIntersect(intersection, b))
return;
intersection = [
comparator[0](intersection[0], b[0]),
comparator[1](intersection[1], b[1]),
comparator[2](intersection[2], b[2]),
comparator[3](intersection[3], b[3]),
];
}
return intersection;
};
/**
* Returns true if the two bounds intersect.
* @author m1b
* @version 2024-03-10
* @param {Array} bounds1 - bounds array.
* @param {Array} bounds2 - bounds array.
* @param {Boolean} [TLBR] - whether bounds arrays are interpreted as [t, l, b, r] or [l, t, r, b] (default: based on app).
* @returns {Boolean}
*/
function boundsDoIntersect(bounds1, bounds2, TLBR) {
if (undefined == TLBR)
TLBR = (/indesign/i.test(app.name));
return !(
TLBR
// TLBR
? (
bounds2[0] > bounds1[2]
|| bounds2[1] > bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] < bounds1[1]
)
// LTRB
: (
bounds2[0] > bounds1[2]
|| bounds2[1] < bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] > bounds1[1]
)
);
};
Edit 2025-02-25: added a check for a filled or stroked clipping mask.
Copy link to clipboard
Copied
Hello @m1b works great, what I noticed when the content inside is small and does not protrude from the mask does not stack correctly, I added a preview and overridden the stroke thickness when aligning.
(function () {
var doc = app.activeDocument;
var selx = doc.selection;
if (selx.length < 2) {
alert("Por favor, selecciona al menos 2 objetos.");
} else {
var initialPositions = saveInitialPositions(selx); // Guardar posiciones iniciales
var userValues = showDialog(initialPositions);
if (!userValues) {
restoreInitialPositions(selx, initialPositions); // Restaurar si se cancela
}
}
})();
function saveInitialPositions(selection) {
var positions = [];
for (var i = 0; i < selection.length; i++) {
var bounds = getItemBoundsIllustrator(selection[i], true);
positions.push({x: bounds[0], y: bounds[1]});
}
return positions;
}
function restoreInitialPositions(selection, positions) {
for (var i = 0; i < selection.length; i++) {
var bounds = getItemBoundsIllustrator(selection[i], true);
var dx = positions[i].x - bounds[0];
var dy = positions[i].y - bounds[1];
selection[i].translate(dx, dy, true, true, true, true);
}
app.redraw();
}
function showDialog(initialPositions) {
var dialog = new Window('dialog', 'Distribuir Objetos Apilados');
dialog.add('statictext', undefined, 'Separación:');
var separationInput = dialog.add('edittext', undefined, '1');
separationInput.characters = 5;
var unitsGroup = dialog.add('group');
var cmButton = unitsGroup.add('radiobutton', undefined, 'cm');
var mmButton = unitsGroup.add('radiobutton', undefined, 'mm');
var pxButton = unitsGroup.add('radiobutton', undefined, 'px');
cmButton.value = true;
dialog.add('statictext', undefined, 'Modo de distribución:');
var modeGroup = dialog.add('group');
var gridButton = modeGroup.add('radiobutton', undefined, 'Grid');
var horizontalButton = modeGroup.add('radiobutton', undefined, 'Horizontal');
var verticalButton = modeGroup.add('radiobutton', undefined, 'Vertical');
verticalButton.value = true;
dialog.add('statictext', undefined, 'Columnas (solo para Grid):');
var columnsInput = dialog.add('edittext', undefined, '3');
columnsInput.characters = 5;
var previewCheckbox = dialog.add('checkbox', undefined, 'Vista previa');
previewCheckbox.value = false;
var buttons = dialog.add('group');
buttons.alignment = 'center';
var cancelButton = buttons.add('button', undefined, 'Cancelar', { name: 'cancel' });
var okButton = buttons.add('button', undefined, 'Aceptar', { name: 'ok' });
okButton.onClick = function () {
dialog.close(1);
};
cancelButton.onClick = function () {
dialog.close(0);
};
// Función para actualizar la vista previa
function updatePreview() {
if (previewCheckbox.value) {
var userValues = {
separation: parseFloat(separationInput.text),
unit: cmButton.value ? 'cm' : (mmButton.value ? 'mm' : 'px'),
mode: gridButton.value ? 'grid' : (horizontalButton.value ? 'horizontal' : 'vertical'),
columns: parseInt(columnsInput.text)
};
distributeObjects(app.activeDocument.selection, userValues, true); // Vista previa
} else {
restoreInitialPositions(app.activeDocument.selection, initialPositions); // Restaurar estado inicial
}
}
// Escuchar cambios en los controles para actualizar la vista previa
separationInput.onChange = unitsGroup.onClick = columnsInput.onChange = function () {
updatePreview();
};
// Escuchar cambios en los botones de modo de distribución
gridButton.onClick = horizontalButton.onClick = verticalButton.onClick = function () {
updatePreview();
};
// Escuchar el clic en la casilla de vista previa
previewCheckbox.onClick = function () {
updatePreview();
};
if (dialog.show() == 1) {
return {
separation: parseFloat(separationInput.text),
unit: cmButton.value ? 'cm' : (mmButton.value ? 'mm' : 'px'),
mode: gridButton.value ? 'grid' : (horizontalButton.value ? 'horizontal' : 'vertical'),
columns: parseInt(columnsInput.text)
};
}
return null;
}
function distributeObjects(sel, userValues, isPreview) {
var separation = userValues.separation;
var unitMultiplier = (userValues.unit == 'cm') ? 28.3465 : (userValues.unit == 'mm' ? 2.83465 : 1);
separation *= unitMultiplier;
var mode = userValues.mode;
var gridCols = (mode == 'grid') ? userValues.columns : (mode == 'horizontal' ? sel.length : 1);
var currentX = 0, currentY = 0, maxRowH = 0;
if (mode == 'horizontal' || mode == 'grid') {
sel.sort(function (a, b) {
return getItemBoundsIllustrator(a, true)[0] - getItemBoundsIllustrator(b, true)[0];
});
} else if (mode == 'vertical') {
sel.sort(function (a, b) {
return getItemBoundsIllustrator(a, true)[3] - getItemBoundsIllustrator(b, true)[3];
});
}
for (var i = 0; i < sel.length; i++) {
var bounds = getItemBoundsIllustrator(sel[i], true);
var objectWidth = bounds[2] - bounds[0];
var objectHeight = bounds[1] - bounds[3];
var dx = currentX - bounds[0];
var dy = currentY - bounds[1];
sel[i].translate(dx, dy, true, true, true, true);
if (mode == 'horizontal') {
currentX += (objectWidth + separation);
} else if (mode == 'vertical') {
currentY -= (objectHeight + separation);
} else if (mode == 'grid') {
currentX += (objectWidth + separation);
if ((i % gridCols) == (gridCols - 1)) {
currentX = 0;
currentY -= (maxRowH + separation);
maxRowH = 0;
}
maxRowH = Math.max(maxRowH, objectHeight);
}
}
if (isPreview) {
app.redraw();
}
}
function getItemBoundsIllustrator(item, geometric, bounds) {
var newBounds = [],
boundsKey = geometric ? 'geometricBounds' : 'visibleBounds';
if (undefined == item)
return;
if (
item.typename == 'GroupItem'
|| item.constructor.name == 'Array'
) {
var children = item.typename == 'GroupItem' ? item.pageItems : item,
contentBounds = [],
isClippingGroup = (item.hasOwnProperty('clipped') && item.clipped == true),
clipBounds;
for (var i = 0, child; i < children.length; i++) {
child = children[i];
if (
child.hasOwnProperty('clipping')
&& true === child.clipping
)
clipBounds = child.geometricBounds;
else
contentBounds.push(getItemBoundsIllustrator(child, geometric, bounds));
}
newBounds = combineBounds(contentBounds);
if (isClippingGroup)
newBounds = intersectionOfBounds([clipBounds, newBounds]);
} else if (
'TextFrame' === item.constructor.name
&& TextType.AREATEXT !== item.kind
) {
var dup = item.duplicate().createOutline();
newBounds = dup[boundsKey];
dup.remove();
} else if (item.hasOwnProperty(boundsKey)) {
newBounds = item[boundsKey];
}
bounds = (undefined == bounds)
? bounds = newBounds
: bounds = combineBounds([newBounds, bounds]);
return bounds;
}
function combineBounds(boundsArray) {
var combinedBounds = boundsArray[0],
comparator;
if (/indesign/i.test(app.name))
comparator = [Math.min, Math.min, Math.max, Math.max];
else
comparator = [Math.min, Math.max, Math.max, Math.min];
for (var i = 1; i < boundsArray.length; i++) {
var bounds = boundsArray[i];
combinedBounds = [
comparator[0](combinedBounds[0], bounds[0]),
comparator[1](combinedBounds[1], bounds[1]),
comparator[2](combinedBounds[2], bounds[2]),
comparator[3](combinedBounds[3], bounds[3]),
];
}
return combinedBounds;
}
function intersectionOfBounds(arrayOfBounds) {
var comparator;
if (/indesign/i.test(app.name))
comparator = [Math.max, Math.max, Math.min, Math.min];
else
comparator = [Math.max, Math.min, Math.min, Math.max];
var bounds = arrayOfBounds
.slice(0)
.sort(function (a, b) { return b[0] - a[0] || a[1] - b[1] });
var intersection = bounds.shift(),
b;
while (b = bounds.shift()) {
if (!boundsDoIntersect(intersection, b))
return;
intersection = [
comparator[0](intersection[0], b[0]),
comparator[1](intersection[1], b[1]),
comparator[2](intersection[2], b[2]),
comparator[3](intersection[3], b[3]),
];
}
return intersection;
}
function boundsDoIntersect(bounds1, bounds2, TLBR) {
if (undefined == TLBR)
TLBR = (/indesign/i.test(app.name));
return !(
TLBR
? (
bounds2[0] > bounds1[2]
|| bounds2[1] > bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] < bounds1[1]
)
: (
bounds2[0] > bounds1[2]
|| bounds2[1] < bounds1[3]
|| bounds2[2] < bounds1[0]
|| bounds2[3] > bounds1[1]
)
);
}
Copy link to clipboard
Copied
Hi @Utson_Avila2888 I see. Yes I hadn't expected anyone to fill or stroke a clipping mask, but I've updated my code above with a check for it. Should work correctly now.
- Mark
P.S. A couple of notes on posting code: (1) please use the </> button to format your code, and (2) if you are posting someone else's code publicly, it is impolite to remove their authorship notice.
Find more inspiration, events, and resources on the new Adobe Community
Explore Now