Skip to main content
Participant
October 27, 2022
Answered

Creating a rectangle around multiple objects with a script

  • October 27, 2022
  • 3 replies
  • 3796 views

Hi folks,

 

I'm struggling a bit with a script. Here in the forum I saw a script which creates a rectangle around a single object with a certain offset and so on. This script doesn't work unfortunately, once you select multiple objects and want to create a rectangle along those.

 

My workflow by hand currently looks like this. I'm having those objects on the artboard, then I'm creating a rectangle exacly along the borders of all objects (basically recreating the bounding-box with an actual rectangle if you'd like to say so) and then I'm giving the newly created rectangle a black contour with 0.25pts and finally I'm ofsetting the path by 1mm and delete the inner rectangle.

 

So basically starting point on the left, and the end result to the right when I'm doing it by hand.

 

Pretty time consuming and tedious to do, especially because I find it really hard to place the initial rectangle on the first try, without the need to gently fondle it into the right place afterwards.

 

So heres the script I find which would possibly do the work for me, if it was for only one object:

 

function makeRect() {

if (selection.length == 0) {
    alert("No object …");
    return;
}else{
    if (selection.length > 1) {
        alert("Too many objects …");
        return;
    }else{
        var myDoc = app.activeDocument;
        var Sel = myDoc.selection
        var SelVB = Sel[0].visibleBounds;
        var dMarg = 28.3464567;
        var Links = SelVB[0];
        var Oben = SelVB[1];
        var SelBreite = (SelVB[2] - SelVB[0]);
        var SelHoehe = (SelVB[1] - SelVB[3]);
        var newCMYK = new CMYKColor();
        newCMYK.cyan = 100;
        newCMYK.magenta = 0;
        newCMYK.yellow = 0;
        newCMYK.black = 0;
        var myGroup =myDoc.groupItems.add();
        Sel[0].move(myGroup, ElementPlacement.PLACEATBEGINNING);
        var MargBox = myDoc.pathItems.rectangle(Oben+dMarg, Links-dMarg, SelBreite+2*dMarg, SelHoehe+2*dMarg);
        MargBox.stroked = true;
        MargBox.strokeWidth = 10;
        //MargBox.filled = false;
        MargBox.fillColor = newCMYK;
        MargBox.move(myGroup, ElementPlacement.PLACEATEND);
        }
    }
}

makeRect();

The script makes a rectangle, with stroke of 10, cyan fill and an offset of... 28,something - whatever that exactly means - is it px?

However, this doesn't work when it comes down to multiple objects that need to be rectangle-outlined. I mean there's obviously right at the beginning an alert if you select more than one object.

 

So, could someone help me out on this one? You'd really make my day.

 

Best regards,

Thomas

This topic has been closed for replies.
Correct answer m1b

Hi @TW-22341, I spent a lot of time some while ago trying to work out a good way to get an item's bounds, even if it was clipped or whatever, and I made a function that has a good try at doing so, although it won't be perfect in strange case, eg. where effects like drop shadows are added.

I've thrown together a quick script that uses those functions I already wrote and I think it should do what you ask for. Let me know how it goes for you. You can adjust the "settings" object.

- Mark

 

Edit 1: improved handling of text.

Edit 2: simplified code, changed getItemBounds to handle multiple items as parameter, moved rect behind objects as per OP's code.

 

 

 

(function () {

    var mm = 2.834645;

    // the colors of the rectangle
    var black = new GrayColor(),
        cyan = new CMYKColor();

    black.gray = 100
    cyan.cyan = 100;
    cyan.magenta = 0;
    cyan.yellow = 0;
    cyan.black = 0;;

    // user settings:
    var settings = {

        margin: 1 * mm,
        strokeColor: black,
        strokeWidth: 1

    };

    var doc = app.activeDocument,
        items = doc.selection;

    if (items.length == 0) {
        alert('Please make a selection and try again.');
        return;
    }

    var lastItem = items[items.length - 1],
        bounds = getItemBounds(items);

    if (bounds == undefined)
        return;

    // add margin
    bounds[0] -= settings.margin;
    bounds[1] += settings.margin;
    bounds[2] += settings.margin;
    bounds[3] -= settings.margin;

    // draw the rectangle
    var rect = doc.pathItems.rectangle(bounds[1], bounds[0], bounds[2] - bounds[0], bounds[1] - bounds[3]);
    rect.filled = true;
    rect.fillColor = cyan;
    rect.stroked = true;
    rect.strokeWidth = settings.strokeWidth;
    rect.strokeColor = settings.strokeColor;

    // put the rectangle behind all the items
    rect.move(lastItem, ElementPlacement.PLACEAFTER);

    // leave just the rectangle selected
    app.selection = null;
    rect.selected = true;

    // finished


    /**
     * Returns bounds of item.
     * @author m1b
     * @version 2022-10-28
     * Attempts to get correct bounds
     * of clipped groups.
     * @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 getItemBounds(item, geometric, bounds) {

        var newBounds = [];

        if (
            item.typename == 'GroupItem'
            || item.constructor.name == 'Array'
        ) {

            var children = item.typename == 'GroupItem' ? item.pageItems : item,
                contentBounds = [];

            if (
                item.hasOwnProperty('clipped')
                && item.clipped == true
            ) {

                // item is clipping group
                var clipBounds;
                for (var i = 0; i < children.length; i++) {

                    var child = children[i];
                    if (
                        child.hasOwnProperty('clipping')
                        && child.clipping == true
                    )
                        // the clipping item
                        clipBounds = child.geometricBounds;

                    else
                        // a clipped content item
                        var b = expandBounds(getItemBounds(child, geometric, bounds), contentBounds);

                }
                newBounds = intersectionOfBounds([clipBounds, contentBounds]);

            }

            else {

                // item is a normal group
                for (var i = 0; i < children.length; i++) {
                    var child = children[i];
                    var b = expandBounds(getItemBounds(child, geometric, bounds), contentBounds);
                }

                newBounds = contentBounds;

            }

        }

        else if (item.typename == 'TextFrame') {

            // item is a text frame
            var dup = item.duplicate().createOutline();
            newBounds = geometric ? dup.geometricBounds : dup.visibleBounds;
            dup.remove();

        }

        else {

            // item is not clipping group
            newBounds = geometric ? item.geometricBounds : item.visibleBounds;

        }

        bounds = (bounds == undefined)
            ? bounds = newBounds
            : bounds = expandBounds(newBounds, bounds);

        return bounds;

    };


    /**
     * Returns bounds that encompass both bounds.
     * @author m1b
     * @version 2022-07-24
     **
     * @param {Array} b1 - bounds array [l, t, r, b].
     * @param {Array} b2 - bounds array [l, t, r, b].
     * @returns {Array} - the encompassing bounds.
     */
    function expandBounds(b1, b2) {

        var expanded = b2;

        for (var i = 0; i < 4; i++) {
            if (b1[i] != undefined && b2[i] == undefined) expanded[i] = b1[i];
            if (b1[i] == undefined && b2[i] != undefined) expanded[i] = b2[i];
            if (b1[i] == undefined && b2[i] == undefined) return;
        }

        if (b1[0] < b2[0]) expanded[0] = b1[0];
        if (b1[1] > b2[1]) expanded[1] = b1[1];
        if (b1[2] > b2[2]) expanded[2] = b1[2];
        if (b1[3] < b2[3]) expanded[3] = b1[3];

        return expanded;

    };


    /**
     * Returns the overlapping rectangle
     * of two or more rectangles.
     * @author m1b
     * @version 2022-07-24
     * NOTE: Returns undefined if ANY
     * rectangles do not intersect.
     * @param {Array} arrayOfBounds - an array of bounds arrays [l, t, r, b].
     * @returns {Array} - bounds array [l, t, r, b] of overlap.
     */
    function intersectionOfBounds(arrayOfBounds) {

        // 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;

            var l = Math.max(intersection[0], b[0]),
                t = Math.min(intersection[1], b[1]),
                r = Math.min(intersection[2], b[2]),
                b = Math.max(intersection[3], b[3]);

            intersection = [l, t, r, b];

        }

        return intersection;

    };




    /**
     * Returns true if the two bounds intersect.
     * @author m1b
     * @version 2022-10-04
     * @param {Array} bounds1 - bounds array [l, t, r, b].
     * @param {Array} bounds2 - bounds array [l, t, r, b].
     * @param {Boolean} TLBR - whether bounds arrays are interpreted as [t, l, b, r].
     * @returns {Boolean}
     */
    function boundsDoIntersect(bounds1, bounds2, TLBR) {

        if (TLBR === true)
            // TLBR
            return !(
                bounds2[0] > bounds1[2]
                || bounds2[1] > bounds1[3]
                || bounds2[2] < bounds1[0]
                || bounds2[3] < bounds1[1]
            );

        else
            // LTRB
            return !(
                bounds2[0] > bounds1[2]
                || bounds2[1] < bounds1[3]
                || bounds2[2] < bounds1[0]
                || bounds2[3] > bounds1[1]
            );

    };


})();

 

 

3 replies

Kurt Gold
Community Expert
Community Expert
October 29, 2022

Another scripting approach with a pretty versatile dialog would be this one, provided by Sergey Anosov. It still works in recent Illustrator versions.

 

Make Rectangle

 

m1b
Community Expert
m1bCommunity ExpertCorrect answer
Community Expert
October 27, 2022

Hi @TW-22341, I spent a lot of time some while ago trying to work out a good way to get an item's bounds, even if it was clipped or whatever, and I made a function that has a good try at doing so, although it won't be perfect in strange case, eg. where effects like drop shadows are added.

I've thrown together a quick script that uses those functions I already wrote and I think it should do what you ask for. Let me know how it goes for you. You can adjust the "settings" object.

- Mark

 

Edit 1: improved handling of text.

Edit 2: simplified code, changed getItemBounds to handle multiple items as parameter, moved rect behind objects as per OP's code.

 

 

 

(function () {

    var mm = 2.834645;

    // the colors of the rectangle
    var black = new GrayColor(),
        cyan = new CMYKColor();

    black.gray = 100
    cyan.cyan = 100;
    cyan.magenta = 0;
    cyan.yellow = 0;
    cyan.black = 0;;

    // user settings:
    var settings = {

        margin: 1 * mm,
        strokeColor: black,
        strokeWidth: 1

    };

    var doc = app.activeDocument,
        items = doc.selection;

    if (items.length == 0) {
        alert('Please make a selection and try again.');
        return;
    }

    var lastItem = items[items.length - 1],
        bounds = getItemBounds(items);

    if (bounds == undefined)
        return;

    // add margin
    bounds[0] -= settings.margin;
    bounds[1] += settings.margin;
    bounds[2] += settings.margin;
    bounds[3] -= settings.margin;

    // draw the rectangle
    var rect = doc.pathItems.rectangle(bounds[1], bounds[0], bounds[2] - bounds[0], bounds[1] - bounds[3]);
    rect.filled = true;
    rect.fillColor = cyan;
    rect.stroked = true;
    rect.strokeWidth = settings.strokeWidth;
    rect.strokeColor = settings.strokeColor;

    // put the rectangle behind all the items
    rect.move(lastItem, ElementPlacement.PLACEAFTER);

    // leave just the rectangle selected
    app.selection = null;
    rect.selected = true;

    // finished


    /**
     * Returns bounds of item.
     * @author m1b
     * @version 2022-10-28
     * Attempts to get correct bounds
     * of clipped groups.
     * @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 getItemBounds(item, geometric, bounds) {

        var newBounds = [];

        if (
            item.typename == 'GroupItem'
            || item.constructor.name == 'Array'
        ) {

            var children = item.typename == 'GroupItem' ? item.pageItems : item,
                contentBounds = [];

            if (
                item.hasOwnProperty('clipped')
                && item.clipped == true
            ) {

                // item is clipping group
                var clipBounds;
                for (var i = 0; i < children.length; i++) {

                    var child = children[i];
                    if (
                        child.hasOwnProperty('clipping')
                        && child.clipping == true
                    )
                        // the clipping item
                        clipBounds = child.geometricBounds;

                    else
                        // a clipped content item
                        var b = expandBounds(getItemBounds(child, geometric, bounds), contentBounds);

                }
                newBounds = intersectionOfBounds([clipBounds, contentBounds]);

            }

            else {

                // item is a normal group
                for (var i = 0; i < children.length; i++) {
                    var child = children[i];
                    var b = expandBounds(getItemBounds(child, geometric, bounds), contentBounds);
                }

                newBounds = contentBounds;

            }

        }

        else if (item.typename == 'TextFrame') {

            // item is a text frame
            var dup = item.duplicate().createOutline();
            newBounds = geometric ? dup.geometricBounds : dup.visibleBounds;
            dup.remove();

        }

        else {

            // item is not clipping group
            newBounds = geometric ? item.geometricBounds : item.visibleBounds;

        }

        bounds = (bounds == undefined)
            ? bounds = newBounds
            : bounds = expandBounds(newBounds, bounds);

        return bounds;

    };


    /**
     * Returns bounds that encompass both bounds.
     * @author m1b
     * @version 2022-07-24
     **
     * @param {Array} b1 - bounds array [l, t, r, b].
     * @param {Array} b2 - bounds array [l, t, r, b].
     * @returns {Array} - the encompassing bounds.
     */
    function expandBounds(b1, b2) {

        var expanded = b2;

        for (var i = 0; i < 4; i++) {
            if (b1[i] != undefined && b2[i] == undefined) expanded[i] = b1[i];
            if (b1[i] == undefined && b2[i] != undefined) expanded[i] = b2[i];
            if (b1[i] == undefined && b2[i] == undefined) return;
        }

        if (b1[0] < b2[0]) expanded[0] = b1[0];
        if (b1[1] > b2[1]) expanded[1] = b1[1];
        if (b1[2] > b2[2]) expanded[2] = b1[2];
        if (b1[3] < b2[3]) expanded[3] = b1[3];

        return expanded;

    };


    /**
     * Returns the overlapping rectangle
     * of two or more rectangles.
     * @author m1b
     * @version 2022-07-24
     * NOTE: Returns undefined if ANY
     * rectangles do not intersect.
     * @param {Array} arrayOfBounds - an array of bounds arrays [l, t, r, b].
     * @returns {Array} - bounds array [l, t, r, b] of overlap.
     */
    function intersectionOfBounds(arrayOfBounds) {

        // 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;

            var l = Math.max(intersection[0], b[0]),
                t = Math.min(intersection[1], b[1]),
                r = Math.min(intersection[2], b[2]),
                b = Math.max(intersection[3], b[3]);

            intersection = [l, t, r, b];

        }

        return intersection;

    };




    /**
     * Returns true if the two bounds intersect.
     * @author m1b
     * @version 2022-10-04
     * @param {Array} bounds1 - bounds array [l, t, r, b].
     * @param {Array} bounds2 - bounds array [l, t, r, b].
     * @param {Boolean} TLBR - whether bounds arrays are interpreted as [t, l, b, r].
     * @returns {Boolean}
     */
    function boundsDoIntersect(bounds1, bounds2, TLBR) {

        if (TLBR === true)
            // TLBR
            return !(
                bounds2[0] > bounds1[2]
                || bounds2[1] > bounds1[3]
                || bounds2[2] < bounds1[0]
                || bounds2[3] < bounds1[1]
            );

        else
            // LTRB
            return !(
                bounds2[0] > bounds1[2]
                || bounds2[1] < bounds1[3]
                || bounds2[2] < bounds1[0]
                || bounds2[3] > bounds1[1]
            );

    };


})();

 

 

TW-22341Author
Participant
October 27, 2022

Thank you, works like a charm up to almost 99%. I found a strange behaviour I didn't expect and can't find out where the script is getting distracted. As long as its basic Shapes it's working as intended. But as soon as there is text along a path it get's distracted and doesn't offset by 1mm as programmed. The example to the right shows it. As you can see in comparison to the actual bounding-box it even offset the rectangle unevenly. Buy overlooking the script I can't see where this might occur. It just seems to be text on the circle there. If I delete that and just put text inside the circle the offset is correctly executed. You got any idea why it is behaving like this?

m1b
Community Expert
Community Expert
October 27, 2022

Okay the problem is that the bounds of textFrames include some "invisible" space, eg. between the cap height and the top of the em square. Same at the bottom, between the descender and the bottom of the em square. I've added a potential fix: to duplicate and outline and get the bounds of that. See what you think. I've updated the script above. - Mark

Doug A Roberts
Community Expert
Community Expert
October 27, 2022

Not a script solution, but you can achieve what you've been doing by hand by grouping the objects, adding a stroke to the group and using Effect > Convert to Shape > Rectangle on the stroke:

You could save that as a graphic style.

 

TW-22341Author
Participant
October 27, 2022

Actually yes, if it was only for the optical effect. But I do need an actual path for anything that comes after this step 🙂

Doug A Roberts
Community Expert
Community Expert
October 27, 2022

Object > Expand Appearance