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:)
I have improved the script I wrote for this answer and you can download it from github repo. The repo has instructions, too. It performs 2D bin packing of the sort that the OP asked about.
- Mark
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.
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.
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.
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]
);
}
Copy link to clipboard
Copied
im shocked . this is by far the best community i have ever found on internet . solutions , passion , responsiveness ideas . I'm speechless .. worth every cents .. this one code was superuseful .. thanks !!!
Copy link to clipboard
Copied
Hi, can it be changed to arrange objects by layer? Thanks a lot
Copy link to clipboard
Copied
Hi @č«å°91371145, I think so. Try commenting out the following lines:
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]);
}
Let me know if it works. š
- Mark
Copy link to clipboard
Copied
hello script cannot be arranged from left to right
Copy link to clipboard
Copied
Oh sorry it's been a long time. I had a quick look at the pin packing part of the code and I couldn't understand how to change the orientation. It will take me some time to look deeper but I'm too busy at the moment unfortunately.
As a workaround, could you try rotating your artwork, running script, then rotating back again? That should work.
- Mark
Copy link to clipboard
Copied
No, it doesn't work, I hope you can fix it when you have time, thank you very much
Copy link to clipboard
Copied
Where can i find this script? I tried copying to notepad and creating a vbs extention but it doesn't work.
Copy link to clipboard
Copied
Hi @colincub, save it as plain text (very important) with ".js" file extension. - Mark
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.
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
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?
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
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.
Copy link to clipboard
Copied
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
Copy link to clipboard
Copied
I have improved the script I wrote for this answer and you can download it from github repo. The repo has instructions, too. It performs 2D bin packing of the sort that the OP asked about.
- Mark