Welcome Dialog

Welcome to the Community!

We have a brand new look! Take a tour with us and explore the latest updates on Adobe Support Community.


How to organize multiple different objects on one sheet with a defined gap inbetween them?

New Here ,
Oct 27, 2021 Oct 27, 2021

Copy link to clipboard

Copied

Say I have a 100 circles in different sizes, and I want illustrator to organize them for me, instead of moving them one by one. Does anyone know a way (script maybe?) to organize them so they are spreaded on the sheet with a defined gap inbetween them, so they dont overlap?
Thanks:)

TOPICS
How to, Scripting, Tools

Views

253

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Oct 27, 2021 Oct 27, 2021

Copy link to clipboard

Copied

ortalk,

 

There are scripts for the purpose of placing objects as tightly as possible, but you may be able to:

 

1) Select all circles and apply Effect>Path>Offset Path by the desired distance, along with ticking Use Preview Bounds in the Preferences,

2) Run the chosen script,

3) Reduce to basic appearance in the Appearance panel.

 

Or you you can start by offsetting all circles by the desired gap, and offset them back when placed.

 

Either way that will give half the gap at all sides; you can change that by also changing the size of the sheet and changing it back afterwards.

 

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Enthusiast ,
Oct 27, 2021 Oct 27, 2021

Copy link to clipboard

Copied

Update: see my post below for an updated script with UI.

 

Hi @ortalk88656280, as @Jacob Bugge mentioned, what you need is a bin packing algorithm. I've had a look around and found one that was pretty simple to convert to Extendscript to use in Illustrator.

 

To use it:

1. Make a selection of page items (eg. your circles; they can be grouped items—the group will be packed as a single item)

2. Make sure the active artboard is the size you need to pack the circles into

3. Run script

 

I'd like to know whether it works well enough for your purpose. I have hardly tested at all, so let me know if it has errors.

- Mark

 

 

 

 

/*

    Pack selected items to artboard

    by m1b, here: https://community.adobe.com/t5/illustrator-discussions/how-to-organize-multiple-different-objects-on-one-sheet-with-a-defined-gap-inbetween-them/m-p/12475475#M295934

    Bin packing algorithm by trentium: https://stackoverflow.com/users/7696162/trentium
    from here: https://stackoverflow.com/questions/56642111/bin-packing-js-implementation-using-box-rotation-for-best-fit
    * modified to conform with ExtendScript syntax and added doRotate flag

*/


Packer2 = function (w, h, doRotate) {
    this.doRotate = (doRotate == true);
    this.init(w, h);
};

Packer2.prototype = {

    init: function (w, h) {
        this._root = { x: 0, y: 0, w: w, h: h }
    },

    intersect: function (block0, block1) {
        //
        // Returns the intersecting block of
        // block0 and block1.
        //
        var ix0 = Math.max(block0.x0, block1.x0);
        var ix1 = Math.min(block0.x1, block1.x1);
        var iy0 = Math.max(block0.y0, block1.y0);
        var iy1 = Math.min(block0.y1, block1.y1);

        if (ix0 <= ix1 && iy0 <= iy1) {
            return { x0: ix0, y0: iy0, x1: ix1, y1: iy1 };
        } else {
            return null;
        }
    },

    chunkContains: function (heapBlock0, heapBlock1) {
        //
        // Determine whether heapBlock0 totally encompasses (ie, contains) heapBlock1.
        //
        return heapBlock0.x0 <= heapBlock1.x0 && heapBlock0.y0 <= heapBlock1.y0 && heapBlock1.x1 <= heapBlock0.x1 && heapBlock1.y1 <= heapBlock0.y1;
    },

    expand: function (heapBlock0, heapBlock1) {
        //
        // Extend heapBlock0 and heapBlock1 if they are
        // adjoining or overlapping.
        //
        if (heapBlock0.x0 <= heapBlock1.x0 && heapBlock1.x1 <= heapBlock0.x1 && heapBlock1.y0 <= heapBlock0.y1) {
            heapBlock1.y0 = Math.min(heapBlock0.y0, heapBlock1.y0);
            heapBlock1.y1 = Math.max(heapBlock0.y1, heapBlock1.y1);
        }

        if (heapBlock0.y0 <= heapBlock1.y0 && heapBlock1.y1 <= heapBlock0.y1 && heapBlock1.x0 <= heapBlock0.x1) {
            heapBlock1.x0 = Math.min(heapBlock0.x0, heapBlock1.x0);
            heapBlock1.x1 = Math.max(heapBlock0.x1, heapBlock1.x1);
        }
    },

    unionMax: function (heapBlock0, heapBlock1) {
        //
        // Given two heap blocks, determine whether...
        //
        if (heapBlock0 && heapBlock1) {
            // ...heapBlock0 and heapBlock1 intersect, and if so...
            var i = this.intersect(heapBlock0, heapBlock1);
            if (i) {
                if (this.chunkContains(heapBlock0, heapBlock1)) {
                    // ...if heapBlock1 is contained by heapBlock0...
                    heapBlock1 = null;
                } else if (this.chunkContains(heapBlock1, heapBlock0)) {
                    // ...or if heapBlock0 is contained by heapBlock1...
                    heapBlock0 = null;
                } else {
                    // ...otherwise, var's expand both heapBlock0 and
                    // heapBlock1 to encompass as much of the intersected
                    // space as possible.  In this instance, both heapBlock0
                    // and heapBlock1 will overlap.
                    this.expand(heapBlock0, heapBlock1);
                    this.expand(heapBlock1, heapBlock0);
                }
            }
        }
    },

    unionAll: function () {
        //
        // Loop through the entire heap, looking to eliminate duplicative
        // heapBlocks, and to extend adjoining or intersecting heapBlocks,
        // despite this introducing overlapping heapBlocks.
        //
        for (var i = 0; i < this.heap.length; i++) {
            for (var j = 0; j < this.heap.length; j++) {
                if (i !== j) {
                    this.unionMax(this.heap[i], this.heap[j]);
                    if (this.heap[i] && this.heap[j]) {
                        if (this.chunkContains(this.heap[j], this.heap[i])) {
                            this.heap[i] = null;
                        } else if (this.chunkContains(this.heap[i], this.heap[j])) {
                            this.heap[j] = null;
                        }
                    }
                }
            }
        }
        // Eliminate the duplicative (ie, nulled) heapBlocks.
        var onlyBlocks = [];
        for (var i = 0; i < this.heap.length; i++) {
            if (this.heap[i]) {
                onlyBlocks.push(this.heap[i]);
            }
        }
        this.heap = onlyBlocks;
    },

    fit: function (blocks) {
        //
        // Loop through all the blocks, looking for a heapBlock
        // that it can fit into.
        //
        this.heap = [{ x0: 0, y0: 0, x1: this._root.w, y1: this._root.h }];
        var n, node, block;
        for (n = 0; n < blocks.length; n++) {
            block = blocks[n];
            block.rotate = false;
            if (this.findInHeap(block)) {
                this.adjustHeap(block);
            } else if (this.doRotate) {
                // If the block didn't fit in its current orientation,
                // rotate its dimensions and look again.
                var w = block.w;
                block.w = block.h;
                block.h = w;
                block.rotate = true;
                if (this.findInHeap(block)) {
                    this.adjustHeap(block);
                }
            }
        }
    },

    findInHeap: function (block) {
        //
        // Find a heapBlock that can contain the block.
        //
        for (var i = 0; i < this.heap.length; i++) {
            var heapBlock = this.heap[i];
            if (heapBlock && block.w <= heapBlock.x1 - heapBlock.x0 && block.h <= heapBlock.y1 - heapBlock.y0) {
                block.x0 = heapBlock.x0;
                block.y0 = heapBlock.y0;
                block.x1 = heapBlock.x0 + block.w;
                block.y1 = heapBlock.y0 + block.h;
                return true;
            }
        }
        return false;
    },

    adjustHeap: function (block) {
        //
        // Find all heap entries that intersect with block,
        // and adjust the heap by breaking up the heapBlock
        // into the possible 4 blocks that remain after
        // removing the intersecting portion.
        //
        var n = this.heap.length;
        for (var i = 0; i < n; i++) {
            var heapBlock = this.heap[i];
            var overlap = this.intersect(heapBlock, block);
            if (overlap) {

                // Top
                if (overlap.y1 !== heapBlock.y1) {
                    this.heap.push({
                        x0: heapBlock.x0,
                        y0: overlap.y1,
                        x1: heapBlock.x1,
                        y1: heapBlock.y1
                    });
                }

                // Right
                if (overlap.x1 !== heapBlock.x1) {
                    this.heap.push({
                        x0: overlap.x1,
                        y0: heapBlock.y0,
                        x1: heapBlock.x1,
                        y1: heapBlock.y1
                    });
                }

                // Bottom
                if (heapBlock.y0 !== overlap.y0) {
                    this.heap.push({
                        x0: heapBlock.x0,
                        y0: heapBlock.y0,
                        x1: heapBlock.x1,
                        y1: overlap.y0
                    });
                }

                // Left
                if (heapBlock.x0 != overlap.x0) {
                    this.heap.push({
                        x0: heapBlock.x0,
                        y0: heapBlock.y0,
                        x1: overlap.x0,
                        y1: heapBlock.y1
                    });
                }

                this.heap[i] = null;
            }
        }

        this.unionAll();
    }

}


packItems(

    // document
    app.activeDocument,

    // page items (can be groupItems)
    app.activeDocument.selection,

    // padding in pts (space between items)
    10,

    // is it okay to rotate 90 degrees?
    false,

    // sort method (can be area, maxDimension, maxWidth, maxHeight, or undefined)
    'maxDimension',

    // bin size in pts [width, height]
    // leave undefined to use active artboard size
    [undefined, undefined]

);


function packItems(doc, items, padding, okToRotateItems, sortBy, binSize) {
    padding = padding || 0;
    binSize = binSize || [];
    if (binSize.width == undefined || binSize.height == undefined) {
        var rect = doc.artboards[doc.artboards.getActiveArtboardIndex()].artboardRect;
        binSize[0] = rect[2] - rect[0] + padding;
        binSize[1] = rect[3] - rect[1] + padding;
    }

    // keep an array of 'blocks' which store positioning information
    var blocks = [];
    for (var i = 0; i < items.length; i++)
        blocks.push(makeBlockForItem(doc, items[i], padding));

    // sort prior to packing
    switch (sortBy) {
        case 'area':
            blocks.sort(function (a, b) { return (b.w * b.h) - (a.w * a.h) });
            break;
        case 'maxDimension':
            blocks.sort(function (a, b) { return Math.max(b.w, b.h) - Math.max(a.w, a.h) });
            break;
        case 'maxWidth':
            blocks.sort(function (a, b) { return b.w - a.w });
            break;
        case 'maxHeight':
            blocks.sort(function (a, b) { return b.h - a.h });
            break;
        default:
            // don't sort
            break;
    }

    // instantiate Trentium's packer
    var packer = new Packer2(binSize[0], -binSize[1], okToRotateItems);

    // do the fitting
    packer.fit(blocks);

    // position the items
    for (var i = 0; i < blocks.length; i++)
        positionBlockItem(blocks[i], padding);

    function makeBlockForItem(doc, item) {
        return {
            w: item.width + padding,
            h: item.height + padding,
            doc: doc,
            item: item
        }
    }

    function positionBlockItem(block) {
        if (!block.hasOwnProperty('x0')) {
            // didn't fit
            block.item.selected = false;
            return;
        }
        if (block.rotate) block.item.rotate(90);
        block.item.left = block.x0;
        block.item.top = -block.y0;
    }
}

 

Edit: fixed order of script functions. To change the settings, look for the call to packItems.

 

 

 

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Oct 28, 2021 Oct 28, 2021

Copy link to clipboard

Copied

Very good approach, Mark.

 

I did some testing and so far your script works very well. Thanks for sharing.

 

I can imagine that it would be nice to incorporate a dialog to control the script settings. Perhaps something similar to Alexander Ladygin's Harmonizer script.

 

https://community.adobe.com/t5/illustrator-discussions/how-to-distribute-objects-inside-another-obje...

 

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Enthusiast ,
Oct 29, 2021 Oct 29, 2021

Copy link to clipboard

Copied

Good idea! I've put a UI on the script above. Also I made a couple of improvemnents:

 

1. Now the fitting algorithm is run multiple times (see the Max Attempts value), changing the order (and possibly rotation) each time. It is a non-intelligent, naive approach, but it does improve the result in many cases. If you don't have many items to pack, you can crank it up to several thousand attempts. If you have many items to pack, it might take too long. The first four attempts cycle through the sort modes considered most-promising and the fifth and subsequent attempts are just random. It is interesting to look at the results text afterwards—sometimes the best result occurred after a thousand random attempts—but mostly it's much earlier.

 

2. The script now tries to pack any available artboards in the document, starting from the first. So to use the script, set up your artboards how you want them.

 

Usage: set up artboards that you want packed, select the items to pack, then run script. Note that an "item" can be a group or any page item.

 

I've not tested it much, and I'm sure it'll break in some cases, so please let me know how it goes. I will try to edit the script in this post to fix problems.

- Mark

 

Update: 2021-10-30_2235

- script now iterates over, and scores, each attempt, rather than each artboard's attempt (the last version may give a worse result even if the early artboards are good)

- added a random button to the UI—this is fun to do some quick, interactive packing, but is unlikely to provide the strongest packing.

- added a Try harder checkbox—select this to stop the script from stopping at the first viable solution (ie. no remaining unpacked items)

 

Update: 2021-11-02_0856

- script now handles clipped objects correctly (I think!). It's general solution which may be handy for other scripts (see function getItemBounds).

- added an option (in script settings object, not UI) for showing the packing grid—just for debugging

 

Update: 2021-11-07_1422

- script now handles groups with nested clipping groups

 

 

/*

    Pack selected items onto artboards

    by m1b: https://community.adobe.com/t5/user/viewprofilepage/user-id/13791991

    here: https://community.adobe.com/t5/illustrator-discussions/how-to-organize-multiple-different-objects-on-one-sheet-with-a-defined-gap-inbetween-them/m-p/12475475#M295934

    version: 2021-11-07_1412

    Bin packing algorithm by trentium: https://stackoverflow.com/users/7696162/trentium
    from here: https://stackoverflow.com/questions/56642111/bin-packing-js-implementation-using-box-rotation-for-best-fit
    * modified to conform with ExtendScript syntax and minor functionality I wanted

    Notes:
    - script will pack items into available artboards, so set those up according
      to your packing needs.
    - if you enable 'Try Harder' the script won't stop at the first adequate packing
      attempt (ie. no unpacked items); mostly this is fine, but sometimes further
      attempts can yield better a result. Look at the results text to see which attempt
      was chosen as best. Go and make yourself a coffee while it tries thousands of
      attempts: sometimes it will hit on a winner.
    - the first 4 attempts always use pre-set sort functions, chosen as being most likely
      to provide a good result; after that every attempt does a random shuffle.
    - the pre-calculated Max Attempts number is just a very rough guide and doesn't mean much.
    - the Random button runs a single, random-ordered packing without dismissing the dialog,
      which could be handy if you're looking for an aesthetic result.
    - scoring system but favours packing more items in fewer bins with less remaining.
    - scoring can be weighted somewhat to maximize area or maximize item count.
    - padding or margin can be negative (to cause overlaps).
    - comment out the settings.ui line to run with no UI.
    - the term 'bin' is used synonymously with 'artboard' in this script, from 'bin packing'.

*/

try {

    var settings = {

        // document
        document: app.activeDocument,

        // page items (can be groupItems)
        items: app.activeDocument.selection,

        // space between items, in pts, or can use 'mm' or 'inch'
        padding: '1mm',

        // space around edges of artboards, in pts, or can use 'mm' or 'inch'
        margin: '5mm',

        // is it okay to rotate 90 degrees?
        allowRotation: true,

        // which is better:
        // fit more items? use 'count' (default if left undefined)
        // fit larger area of items? use 'area'
        bestFitBy: 'count',

        // the number of attempts to fit items;
        // more attempts sometimes works better
        // but often doesn't
        // leave undefined to auto-calculate
        maxAttempts: undefined,

        // should we stop on first successul packing,
        // or keep trying to improve?
        tryHarder: false,

        // leave undefined if don't need UI
        ui: packingDialog,

        // debugging options
        showGrid: false,
        showOnlyGrid: false

    }

} catch (error) {
    alert('Please select some items and try again.')
}

function packingDialog(packFunction) {
    settings.packFunction = packFunction;
    var dialog = makeUI();
    if (dialog == undefined) return;
    dialog.center();
    var result = dialog.show();
    dialog = null;
    if (result == 1) {
        // do the packing
        // show progress window
        var pb = makeProgressWindow();
        if (pb == undefined) return;
        settings.pb = pb;
        pb.center();
        pb.show();
        packFunction(settings);
    }

    // the packing settings UI
    function makeUI() {
        var doc = settings.document,
            items = settings.items,
            padding = settings.padding || '0 mm',
            margin = settings.margin || '0 mm',
            maxAttempts = settings.maxAttempts || Math.round((4 + (0.75 / items.length * 5000)));

        var w = new Window("dialog", 'Pack Items', undefined, { closeButton: false });
        w.preferredSize.width = 250;

        var introGroup = w.add('group {orientation:"column", alignChildren: "fill", alignment: ["fill","top"], margins: [15,15,15,15] }');
        introGroup.add('statictext { text:"Trying to pack ' + settings.items.length + ' items onto ' + settings.document.artboards.length + ' artboards", justify: "center" }');

        var panelGroup = w.add('group {orientation:"row", alignChildren:["left","top"] }');
        var panel1 = panelGroup.add('panel');
        var panel2 = panelGroup.add('panel');

        var paddingGroup = panel1.add("group {orientation:'column', alignment:['center','top'], alignChildren: ['left','top'], margins:[0,10,0,0], preferredSize: [120,-1] }"),
            paddingLabel = paddingGroup.add('statictext { text: "Space between items:" }'),
            paddingField = paddingGroup.add('edittext {text: "' + padding + '", preferredSize: [120,-1] }');

        var marginGroup = panel1.add("group {orientation:'column', alignment:['center','top'], alignChildren: ['left','top'], margins:[0,10,0,0], preferredSize: [120,-1] }"),
            marginLabel = marginGroup.add('statictext { text: "Artboard margin:" }'),
            marginField = marginGroup.add('edittext {text: "' + margin + '", preferredSize: [120,-1] }');

        var maxAttemptsGroup = panel2.add('group {orientation:"column", alignment:["center","top"], alignChildren: ["left","top"], margins:[0,10,0,0], preferredSize: [120,-1] }'),
            maxAttemptsLabel = maxAttemptsGroup.add('statictext { text:"Max attempts:" }'),
            maxAttemptsField = maxAttemptsGroup.add('edittext { text: "' + maxAttempts + '", preferredSize: [120,-1] }');

        var bestFitGroup = panel2.add('group {orientation:"column", alignment:["center","top"], alignChildren: ["left","top"], margins:[0,10,0,0], preferredSize: [120,-1] }'),
            bestFitLabel = bestFitGroup.add('statictext { text:"Maximize:" }'),
            bestFitMenu = bestFitGroup.add('dropDownList { preferredSize:[120,-1] }');

        var checkboxGroup = panel2.add('group {orientation:"column", alignment:["center","top"], alignChildren: ["left","top"], margins:[0,20,0,0], preferredSize: [120,-1] }'),
            allowRotationCheckbox = checkboxGroup.add("Checkbox { alignment:'left', text:'Allow 90° rotation', margins:[0,10,0,0], value:" + settings.allowRotation + " }"),
            tryHarderCheckbox = checkboxGroup.add("Checkbox { alignment:'left', text:'Try harder', margins:[0,10,0,0], value:" + settings.tryHarder + " }");

        var buttonGroup = w.add('group {orientation:"row", alignment:["center","bottom"], alignChildren: ["right","bottom"], margins: [0,-5,0,0] }'),
            randomGroup = buttonGroup.add('group {orientation:"column", alignment:["center","bottom"], alignChildren: ["right","bottom"], margins: [0,0,50,0] }'),
            randomResult = randomGroup.add('statictext { text:"", alignment: ["fill","bottom"], justify: "center" }'),
            randomButton = randomGroup.add('button', undefined, 'Random'),
            cancelButton = buttonGroup.add('button', undefined, 'Cancel', { name: 'cancel' }),
            packButton = buttonGroup.add('button', undefined, 'Pack', { name: 'ok' });

        bestFitMenu.add('item', 'Items packed');
        bestFitMenu.add('item', 'Area packed');
        bestFitMenu.selection = 0;

        function updateSettings() {
            settings.padding = paddingField.text;
            settings.margin = marginField.text;
            settings.maxAttempts = Number(maxAttemptsField.text);
            settings.bestFitBy = bestFitMenu.selection.index == 0 ? 'count' : 'area';
            settings.allowRotation = allowRotationCheckbox.value;
            settings.tryHarder = tryHarderCheckbox.value;
        }

        randomButton.onClick = function () {
            if (settings.lastAttemptWasRandom == true)
                undoRandomAttempt();
            updateSettings();
            settings.randomAttempt = true;
            var result = settings.packFunction(settings);
            randomResult.text = result.packedItemCount + ' / ' + result.totalItemCount;
            // randomResult.text = result.score;
            settings.lastAttemptWasRandom = true;
            settings.randomAttempt = false;
            app.redraw();
        }

        function undoRandomAttempt() {
            app.undo();
            settings.lastAttemptWasRandom = false;
        }

        packButton.onClick = function () {
            if (settings.lastAttemptWasRandom == true)
                undoRandomAttempt();

            updateSettings();
            w.close(1);
        };

        return w;
    }

    // the progress bars and results UI
    function makeProgressWindow() {
        if (!w)
            var w = new Window('window', 'Packing Items', undefined, { closeButton: false, resize: true });

        var itemsPackedGroup = w.add("group {orientation:'column', alignChildren: 'fill', alignment:['fill','top'], margins: [15,15,15,15] }"),
            pb1Label = itemsPackedGroup.add('statictext { text:"Items packed" }'),
            pb1Row = itemsPackedGroup.add("group {orientation:'row', alignChildren: 'fill', alignment:['fill','top'], margins: [15,15,15,15] }"),
            pb1 = pb1Row.add('progressbar { bounds: [12, 12, 400, 12], value: 0, maxvalue: 100 }');
        pb1.display = pb1Row.add('statictext { text:"1 / 1", size:[100,24] }');

        w.stack = w.add("group {orientation:'stack', alignment:['fill','fill']}");
        var progressGroup = w.stack.add("group {orientation:'column', alignChildren: 'fill', alignment:['fill','fill'] }");

        var attemptsGroup = progressGroup.add("group {orientation:'column', alignChildren: 'fill', alignment:['fill','top'], margins: [15,15,15,15] }"),
            pb2Label = attemptsGroup.add('statictext { text:"Attempt number" }'),
            pb2Row = attemptsGroup.add("group {orientation:'row', alignChildren: 'fill', alignment:['fill','top'], margins: [15,15,15,15] }"),
            pb2 = pb2Row.add('progressbar { bounds: [12, 12, 400, 12], value: 0, maxvalue: 100 }');
        pb2.display = pb2Row.add('statictext { text:"1 / 1", minimumSize: [100,24] }');

        var resultsGroup = w.stack.add("group {orientation:'column', alignChildren: ['fill','fill'], alignment: ['fill','fill'], margins: [15,0,15,0], visible: false }"),
            infoText = resultsGroup.add('statictext { text:"results", preferredSize: [-1,100], properties: { multiline: true } }');

        var resultsButtons = resultsGroup.add("group {orientation:'row', alignment:['right','bottom'], scrolling: true }"),
            doneButton = resultsButtons.add('button', undefined, 'Done', { name: 'ok' });

        doneButton.onClick = function () { w.close(1) };
        w.defaultElement = doneButton;

        w.setProgress = function (pbIndex, value, maxValue) {
            var pb = [pb1, pb2][pbIndex - 1];
            pb.value = value;
            if (maxValue != undefined)
                pb.maxvalue = maxValue;
            pb.display.text = value + ' / ' + maxValue;
            w.update();
        }

        w.setBestBinCount = function (binCount, attempt) {
            pb1Label.text = 'Items packed in ' + binCount + ' artboards on attempt ' + attempt + '.';
        }

        w.showResults = function (text, n) {
            infoText.text = text;
            infoText.size.height = n * 23;
            resultsGroup.visible = true;
            progressGroup.visible = false;
            w.update();
        }

        return w;
    }
}

// Blocks are used to keep track
// of items during packing
function Block(doc, item, margin, padding) {
    var bounds = getItemBounds(item);
    this.doc = doc;
    this.item = item;
    this.margin = margin;
    this.padding = padding;
    this.isRotated = false;
    this.binIndex = undefined;
    this.w = bounds[2] - bounds[0] + this.padding;
    this.h = bounds[1] - bounds[3] + this.padding;
    this.dx = item.left - bounds[0];
    this.dy = item.top - bounds[1];
    this.dimensions = {
        w: this.w,
        h: this.h,
        dx: this.dx,
        dy: this.dy
    };
    this.rotatedDimensions = {
        w: this.h,
        h: this.w,
        dx: -this.dy,
        dy: item.visibleBounds[2] - bounds[2]
    }
}

// swap block between 0 and 90 degree rotation
Block.prototype.rotate = function () {
    this.isRotated = !this.isRotated;
    this.w = this.isRotated ? this.rotatedDimensions.w : this.dimensions.w;
    this.h = this.isRotated ? this.rotatedDimensions.h : this.dimensions.h;
    this.dx = this.isRotated ? this.rotatedDimensions.dx : this.dimensions.dx;
    this.dy = this.isRotated ? this.rotatedDimensions.dy : this.dimensions.dy;
}

// actually position the item
// where it will be packed
Block.prototype.positionItem = function () {
    if (!this.packed) return;

    if (this.isRotated) this.item.rotate(90);

    var artboardRect = this.doc.artboards[this.binIndex].artboardRect,
        l = this.x0 + artboardRect[0] + this.margin,
        t = -(this.y0 - artboardRect[1] + this.margin),
        r = this.x1 + artboardRect[0] + this.margin,
        b = -(this.y1 - artboardRect[1] + this.margin);

    if (settings.showGrid) var r = rectanglePathItem([l, t, r, b]);
    if (settings.showOnlyGrid) return;

    // position the item
    this.item.left = l + this.dx;
    this.item.top = t + this.dy;
}

// just for debugging
Block.prototype.toString = function () {
    if (!this.packed) return '[Object Block, not packed]';
    return '[Object Block, bin:' + this.binIndex
        + ', isRotated:' + this.isRotated
        + ', x0:' + this.x0
        + ', y0:' + this.y0
        + ', w:' + this.w
        + ', h:' + this.h
        + ']';
}

// randomises order of an array
if (!Array.prototype.shuffle) Array.prototype.shuffle = (function (Object, max, min) {
    "use strict";
    return function shuffle(picks) {
        if (this === null || this === undefined) throw TypeError("Array.prototype.shuffle called on null or undefined");

        var p = picks === null || picks === undefined || picks == 0 ? this.length : picks;
        var i = this.length, j = 0, temp;

        while (i--) {
            j = Math.floor(Math.random() * (i + 1));
            // swap randomly chosen element with current element
            temp = this[i];
            this[i] = this[j];
            this[j] = temp;
        }
        return this.slice(0, p);
    };
})(Object, Math.max, Math.min);



(function () {

    Packer2 = function (w, h, allowRotation) {
        this.allowRotation = (allowRotation == true);
        this.init(w, h);
    };

    Packer2.prototype = {

        init: function (w, h) {
            this._root = { x: 0, y: 0, w: w, h: h }
        },

        intersect: function (block0, block1) {
            //
            // Returns the intersecting block of
            // block0 and block1.
            //
            var ix0 = Math.max(block0.x0, block1.x0);
            var ix1 = Math.min(block0.x1, block1.x1);
            var iy0 = Math.max(block0.y0, block1.y0);
            var iy1 = Math.min(block0.y1, block1.y1);

            if (ix0 <= ix1 && iy0 <= iy1) {
                return { x0: ix0, y0: iy0, x1: ix1, y1: iy1 };
            } else {
                return null;
            }
        },

        chunkContains: function (heapBlock0, heapBlock1) {
            //
            // Determine whether heapBlock0 totally encompasses (ie, contains) heapBlock1.
            //
            return heapBlock0.x0 <= heapBlock1.x0 && heapBlock0.y0 <= heapBlock1.y0 && heapBlock1.x1 <= heapBlock0.x1 && heapBlock1.y1 <= heapBlock0.y1;
        },

        expand: function (heapBlock0, heapBlock1) {
            //
            // Extend heapBlock0 and heapBlock1 if they are
            // adjoining or overlapping.
            //
            if (heapBlock0.x0 <= heapBlock1.x0 && heapBlock1.x1 <= heapBlock0.x1 && heapBlock1.y0 <= heapBlock0.y1) {
                heapBlock1.y0 = Math.min(heapBlock0.y0, heapBlock1.y0);
                heapBlock1.y1 = Math.max(heapBlock0.y1, heapBlock1.y1);
            }

            if (heapBlock0.y0 <= heapBlock1.y0 && heapBlock1.y1 <= heapBlock0.y1 && heapBlock1.x0 <= heapBlock0.x1) {
                heapBlock1.x0 = Math.min(heapBlock0.x0, heapBlock1.x0);
                heapBlock1.x1 = Math.max(heapBlock0.x1, heapBlock1.x1);
            }
        },

        unionMax: function (heapBlock0, heapBlock1) {
            //
            // Given two heap blocks, determine whether...
            //
            if (heapBlock0 && heapBlock1) {
                // ...heapBlock0 and heapBlock1 intersect, and if so...
                var i = this.intersect(heapBlock0, heapBlock1);
                if (i) {
                    if (this.chunkContains(heapBlock0, heapBlock1)) {
                        // ...if heapBlock1 is contained by heapBlock0...
                        heapBlock1 = null;
                    } else if (this.chunkContains(heapBlock1, heapBlock0)) {
                        // ...or if heapBlock0 is contained by heapBlock1...
                        heapBlock0 = null;
                    } else {
                        // ...otherwise, var's expand both heapBlock0 and
                        // heapBlock1 to encompass as much of the intersected
                        // space as possible.  In this instance, both heapBlock0
                        // and heapBlock1 will overlap.
                        this.expand(heapBlock0, heapBlock1);
                        this.expand(heapBlock1, heapBlock0);
                    }
                }
            }
        },

        unionAll: function () {
            //
            // Loop through the entire heap, looking to eliminate duplicative
            // heapBlocks, and to extend adjoining or intersecting heapBlocks,
            // despite this introducing overlapping heapBlocks.
            //
            for (var i = 0; i < this.heap.length; i++) {
                for (var j = 0; j < this.heap.length; j++) {
                    if (i !== j) {
                        this.unionMax(this.heap[i], this.heap[j]);
                        if (this.heap[i] && this.heap[j]) {
                            if (this.chunkContains(this.heap[j], this.heap[i])) {
                                this.heap[i] = null;
                            } else if (this.chunkContains(this.heap[i], this.heap[j])) {
                                this.heap[j] = null;
                            }
                        }
                    }
                }
            }
            // Eliminate the duplicative (ie, nulled) heapBlocks.
            var onlyBlocks = [];
            for (var i = 0; i < this.heap.length; i++) {
                if (this.heap[i]) {
                    onlyBlocks.push(this.heap[i]);
                }
            }
            this.heap = onlyBlocks;
        },

        fit: function (blocks, binIndex) {
            //
            // Loop through all the blocks, looking for a heapBlock
            // that it can fit into.
            //
            this.heap = [{ x0: 0, y0: 0, x1: this._root.w, y1: this._root.h }];
            var n, block, area = 0, packedBlocks = [], remainingBlocks = [];
            for (n = 0; n < blocks.length; n++) {
                block = blocks[n];
                if (this.findInHeap(block)) {
                    this.adjustHeap(block);
                } else if (this.allowRotation) {
                    // If the block didn't fit in its current orientation,
                    // rotate its dimensions and look again.
                    block.rotate();
                    if (this.findInHeap(block)) {
                        this.adjustHeap(block);
                    }
                }
                // was it packed?
                if (block.packed) {
                    block.binIndex = binIndex;
                    packedBlocks.push(block);
                    area += block.w * block.h;
                } else {
                    remainingBlocks.push(block);
                }
            }
            return { count: packedBlocks.length, area: area, packedBlocks: packedBlocks, remainingBlocks: remainingBlocks };
        },

        findInHeap: function (block) {
            //
            // Find a heapBlock that can contain the block.
            //
            for (var i = 0; i < this.heap.length; i++) {
                var heapBlock = this.heap[i];
                if (heapBlock && block.w <= heapBlock.x1 - heapBlock.x0 && block.h <= heapBlock.y1 - heapBlock.y0) {
                    block.x0 = heapBlock.x0;
                    block.y0 = heapBlock.y0;
                    block.x1 = heapBlock.x0 + block.w;
                    block.y1 = heapBlock.y0 + block.h;
                    block.packed = true;
                    return true;
                }
            }
            return false;
        },

        adjustHeap: function (block) {
            //
            // Find all heap entries that intersect with block,
            // and adjust the heap by breaking up the heapBlock
            // into the possible 4 blocks that remain after
            // removing the intersecting portion.
            //
            var n = this.heap.length;
            for (var i = 0; i < n; i++) {
                var heapBlock = this.heap[i];
                var overlap = this.intersect(heapBlock, block);
                if (overlap) {

                    // Top
                    if (overlap.y1 !== heapBlock.y1) {
                        this.heap.push({
                            x0: heapBlock.x0,
                            y0: overlap.y1,
                            x1: heapBlock.x1,
                            y1: heapBlock.y1
                        });
                    }

                    // Right
                    if (overlap.x1 !== heapBlock.x1) {
                        this.heap.push({
                            x0: overlap.x1,
                            y0: heapBlock.y0,
                            x1: heapBlock.x1,
                            y1: heapBlock.y1
                        });
                    }

                    // Bottom
                    if (heapBlock.y0 !== overlap.y0) {
                        this.heap.push({
                            x0: heapBlock.x0,
                            y0: heapBlock.y0,
                            x1: heapBlock.x1,
                            y1: overlap.y0
                        });
                    }

                    // Left
                    if (heapBlock.x0 != overlap.x0) {
                        this.heap.push({
                            x0: heapBlock.x0,
                            y0: heapBlock.y0,
                            x1: overlap.x0,
                            y1: heapBlock.y1
                        });
                    }

                    this.heap[i] = null;
                }
            }

            this.unionAll();
        }
    }


    // here is where the main code starts
    if (settings) {
        settings.info = [];

        if (settings.items.length > 0) {

            if (settings.ui != undefined) {
                // pack items with UI
                settings.ui(packItems);
            } else {
                // pack items with no UI
                packItems(settings);
            }

        } else {
            alert('No items selected\nPlease select some items to pack and try again.');
        }
    }


    // pack page items according to parameters
    function packItems(p) {

        var doc = p.document || app.activeDocument,
            items = p.items || doc.selection,
            padding = p.padding || 0,
            margin = p.margin || 0,
            allowRotation = p.allowRotation || false,
            bestFitBy = p.bestFitBy || 'count',
            maxAttempts = p.maxAttempts || Math.round((4 + (0.75 / items.length * 5000))),
            randomAttempt = p.randomAttempt || false,
            preferCount = (bestFitBy == 'count'),
            preferArea = (bestFitBy == 'area'),
            pb = settings.pb,
            totalItemCount = items.length,
            totalItemArea = 0;

        if (padding.constructor.name == 'String')
            padding = parseNumberString(padding);

        if (margin.constructor.name == 'String')
            margin = parseNumberString(margin);

        if (randomAttempt == true) maxAttempts = 1;

        // make bins
        var bins = [],
            artboards = doc.artboards;
        for (var b = 0; b < artboards.length; b++) {
            var rect = artboards[b].artboardRect;
            bins.push({
                artboard: artboards[b],
                width: rect[2] - rect[0] + padding - (margin * 2),
                height: rect[3] - rect[1] + padding + (margin * 2)
            });
        }

        if (pb) pb.setProgress(1, 0, totalItemCount);

        // sort functions to be tested in order, before trying random sorting
        var sortFunctions = [
            function (a, b) { return (b.w * b.h) - (a.w * a.h) },
            function (a, b) { return Math.max(b.w, b.h) - Math.max(a.w, a.h) },
            function (a, b) { return b.w - a.w },
            function (a, b) { return b.h - a.h }
        ];

        var bestAttempt = {
            index: 0,
            count: 0,
            area: 0,
            binCount: bins.length,
            blocks: undefined,
            info: undefined,
            score: undefined
        };

        // loop over attempts
        for (var a = 0; a < maxAttempts; a++) {

            if (pb) pb.setProgress(2, a + 1, maxAttempts);

            var attempt = {
                index: a + 1,
                count: 0,
                area: 0,
                binCount: 0,
                packedBlocks: [],
                remainingBlocks: [],
                info: [],
                score: 0
            };

            // make a fresh array of 'blocks' which will store positioning information
            for (var j = 0; j < items.length; j++) {
                attempt.remainingBlocks.push(new Block(doc, items[j], margin, padding));
                if (a == 0) totalItemArea += items[j].width * items[j].height;
            }

            if (randomAttempt == true || attempt.index > sortFunctions.length) {
                // random sort
                attempt.remainingBlocks.shuffle();
            } else {
                // try all the sortFunctions once in order
                attempt.remainingBlocks.sort(sortFunctions[a]);
            }

            // loop over bins
            for (var b = 0; b < bins.length; b++) {
                var bin = bins[b],

                    // instantiate Trentium's packer
                    packer = new Packer2(bin.width, -bin.height, allowRotation),

                    // do the fitting
                    result = packer.fit(attempt.remainingBlocks, b);

                attempt.area += result.area;
                attempt.binCount = b + 1;
                attempt.packedBlocks = attempt.packedBlocks.concat(result.packedBlocks);
                attempt.remainingBlocks = result.remainingBlocks;

                if (result.count == 0) break;

                // calculate score for this bin
                var scoreFactor = (preferCount == true)
                    ? totalItemCount / result.count
                    : totalItemArea / result.area;
                attempt.score += (bins.length / (b + 1)) * scoreFactor * 100;

                // add a line to info for this attempt
                attempt.info.push('Packed ' + result.count + ' items onto artboard ' + (b + 1) + '.');

                if (attempt.remainingBlocks.length == 0) break;

            } // bin loop

            // an attempt with a lower binCount always wins
            attempt.score += (bins.length - attempt.binCount) * 10000;
            attempt.score -= attempt.remainingBlocks.length * 10000;

            // is this the best attempt so far?
            if (attempt.score > bestAttempt.score || bestAttempt.score == undefined) {
                bestAttempt.binCount = attempt.binCount;
                bestAttempt.area = attempt.area;
                bestAttempt.index = attempt.index;
                bestAttempt.info = attempt.info.slice(0);
                bestAttempt.score = attempt.score;
                bestAttempt.packedBlocks = attempt.packedBlocks.slice(0);
                if (pb) {
                    pb.setProgress(1, bestAttempt.packedBlocks.length, totalItemCount);
                    pb.setBestBinCount(bestAttempt.binCount, bestAttempt.index);
                }
                if (settings.tryHarder != true && attempt.remainingBlocks.length == 0) break;
            }
        } // attempts loop

        var finalPackedBlockCount = 0;
        if (bestAttempt.packedBlocks != undefined) {

            finalPackedBlockCount = bestAttempt.packedBlocks.length;

            // position the items from the best attempt
            for (var i = 0; i < finalPackedBlockCount; i++)
                bestAttempt.packedBlocks[i].positionItem();

        }

        if (randomAttempt != true) {

            settings.info = settings.info.concat(bestAttempt.info);

            var remainingBlockCount = totalItemCount - finalPackedBlockCount;
            if (remainingBlockCount > 0)
                settings.info.push(remainingBlockCount + ' item' + (remainingBlockCount > 1 ? 's' : '') + ' remaining.');

            if (pb) {
                pb.setProgress(1, finalPackedBlockCount, totalItemCount);
                pb.showResults(settings.info.join('\n'), settings.info.length);
                pb = null;
            }

            settings = null;
        }

        // return some info
        return { packedItemCount: finalPackedBlockCount, totalItemCount: totalItemCount, score: Math.round(bestAttempt.score) };

    } // end packItems

})();

// returns number as points
// converting from mm or inches
// eg '10 mm' returns 28.34645669,
// '1 inch' returns 72
function parseNumberString(str) {
    var unit = 1,
        inch = 72,
        mm = 2.834645669;
    if (str.search(/mm/) != -1) {
        unit = mm;
    } else if (str.search(/(in|inch|\")/) != -1) {
        unit = inch;
    }
    return (parseFloat(str.match(/[\d.-]+/)[0]) * unit);
}

// just draw a rectangle stroke black stroke
function rectanglePathItem(rect) {
    var doc = app.activeDocument;
    var l = rect[0], t = rect[1], r = rect[2], b = rect[3];
    var K100 = new GrayColor();
    K100.gray = 100;
    var rectangleItem = doc.activeLayer.pathItems.add();
    rectangleItem.setEntirePath(Array(Point(l, t), Array(r, t), Array(r, b), Array(l, b)));
    rectangleItem.closed = true;
    rectangleItem.filled = false;
    rectangleItem.stroked = true;
    rectangleItem.strokeColor = K100;
    return r;
}

// returns visibleBounds or geometricBounds of item
// including correct bounds of clipped group
function getItemBounds(item, geometric, bounds) {
    var newBounds = [];
    if (item.typename == 'GroupItem') {
        var children = item.pageItems,
            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 {
        // item is not clipping group
        newBounds = geometric ? item.geometricBounds : item.visibleBounds;
    }
    if (bounds == undefined) {
        bounds = newBounds;
    } else {
        bounds = expandBounds(newBounds, bounds);
    }
    return bounds;
}

// returns bounds that encompass two 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;
}

function intersectionOfBounds(arrayOfBounds) {
    bounds = arrayOfBounds.slice(0).sort(function (a, b) { return b[0] - a[0] || a[1] - b[1] });
    var intersection = bounds.shift();
    while (b = bounds.shift()) {
        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;
}

function boundsDoIntersect(bounds1, bounds2) {
    return !(
        bounds2[0] > bounds1[2] ||
        bounds2[2] < bounds1[0] ||
        bounds2[1] < bounds1[3] ||
        bounds2[3] > bounds1[1]
    );
}

 

 

 

 

 

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Oct 30, 2021 Oct 30, 2021

Copy link to clipboard

Copied

Mark, thanks for your improved version including a dialog.

 

I did some testing with plain path objects. It worked well.

 

I can imagine that there may be some minor flaws with clipped objects or other nested constructions, but all in all it is a very good approach.

 

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Enthusiast ,
Oct 30, 2021 Oct 30, 2021

Copy link to clipboard

Copied

Hi Kurt, thanks for trying it out. Yes it is a very simple approach and certainly won't handle some page items. I might be able to handle some clipped groups though—I'll try that next. We've had a big storm here and no power for a couple of days (and a couple more at least to come too) so I've had time to fiddle with the script—as long as my laptop and backup battery holds out. Internet is very patchy so I can only post about once a day.

- Mark

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Oct 30, 2021 Oct 30, 2021

Copy link to clipboard

Copied

I don't think that it is a simple approach, Mark. It is a great contribution from you.

 

I hope you are well and good luck while facing the forthcoming storms.

 

May I ask where you are living on the earth?

 

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Enthusiast ,
Nov 01, 2021 Nov 01, 2021

Copy link to clipboard

Copied

Hi Kurt, well I just got power restored. Phew! I live in the hills to the south east of Melbourne, Australia. We have A LOT of very big trees all around here in the hills and when bad storms come (not often, but we've had two bad ones this year), the power usually goes out due to trees falling. I wouldn't change it though, because I love to live amongst trees. Where are you living?

 

I've updated the script posted above again, this time adding support for clipped items.

- Mark

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Adobe Community Professional ,
Nov 01, 2021 Nov 01, 2021

Copy link to clipboard

Copied

 

Good morning, Mark.
 
thank you for the updated script. I hope I can try it out next week.
 
About half of the year I'm living in the southwest of Germany. That's just a couple of miles away from Melbourne, I think.
 
The other half of a year I'm living somewhere else, most of the time in some northern arctic regions in Europe and sometimes in Asia.

 

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines
Enthusiast ,
Nov 06, 2021 Nov 06, 2021

Copy link to clipboard

Copied

LATEST

Hi Kurt, your situation sounds interesting! I've never been to any arctic areas. By the way, I've updated the script above to handle nested clipping groups. It seems to get bogged down when packing large numbers of items, or maybe certain items—not sure yet. Probably poorly optimized.

- Mark

Likes

Translate

Translate

Report

Report
Community guidelines
Be kind and respectful, give credit to the original source of content, and search for duplicates before posting. Learn more
community guidelines