(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.layout = factory());
})(this, (() => {
const binpack = (() => {
class GrowingPacker {
constructor() { }
fit(blocks) {
var n, node, block, len = blocks.length, fit;
var width = len > 0 ? blocks[0].width : 0;
var height = len > 0 ? blocks[0].height : 0;
this.root = { x: 0, y: 0, width: width, height: height };
for (n = 0; n < len; n++) {
block = blocks[n];
if (node = this.findNode(this.root, block.width, block.height)) {
fit = this.splitNode(node, block.width, block.height);
block.x = fit.x;
block.y = fit.y;
}
else {
fit = this.growNode(block.width, block.height);
block.x = fit.x;
block.y = fit.y;
}
}
}
findNode(root, width, height) {
if (root.used)
return this.findNode(root.right, width, height) || this.findNode(root.down, width, height);
else if ((width <= root.width) && (height <= root.height))
return root;
else
return null;
}
splitNode(node, width, height) {
node.used = true;
node.down = { x: node.x, y: node.y + height, width: node.width, height: node.height - height };
node.right = { x: node.x + width, y: node.y, width: node.width - width, height: height };
return node;
}
growNode(width, height) {
var canGrowDown = (width <= this.root.width);
var canGrowRight = (height <= this.root.height);
var shouldGrowRight = canGrowRight && (this.root.height >= (this.root.width + width)); // attempt to keep square-ish by growing right when height is much greater than width
var shouldGrowDown = canGrowDown && (this.root.width >= (this.root.height + height)); // attempt to keep square-ish by growing down when width is much greater than height
if (shouldGrowRight)
return this.growRight(width, height);
else if (shouldGrowDown)
return this.growDown(width, height);
else if (canGrowRight)
return this.growRight(width, height);
else if (canGrowDown)
return this.growDown(width, height);
else
return null; // need to ensure sensible root starting size to avoid this happening
}
growRight(width, height) {
this.root = {
used: true,
x: 0,
y: 0,
width: this.root.width + width,
height: this.root.height,
down: this.root,
right: { x: this.root.width, y: 0, width: width, height: this.root.height }
};
var node;
if (node = this.findNode(this.root, width, height))
return this.splitNode(node, width, height);
else
return null;
}
growDown(width, height) {
this.root = {
used: true,
x: 0,
y: 0,
width: this.root.width,
height: this.root.height + height,
down: { x: 0, y: this.root.height, width: this.root.width, height: height },
right: this.root
};
var node;
if (node = this.findNode(this.root, width, height))
return this.splitNode(node, width, height);
else
return null;
}
}
return (items, options) => {
options = options || {};
var packer = new GrowingPacker();
var inPlace = options.inPlace || false;
var newItems = items.map(item => inPlace ? item : { width: item.width, height: item.height, item: item });
newItems = newItems.toSorted((a, b) => (b.width * b.height) - (a.width * a.height));
packer.fit(newItems);
const ret = {
width: newItems.reduce((curr, item) => Math.max(curr, item.x + item.width), 0),
height: newItems.reduce((curr, item) => Math.max(curr, item.y + item.height), 0)
};
if (!inPlace) {
ret.items = newItems;
}
return ret;
};
})();
const algorithms = {
'top-down': {
sort: items => items.toSorted((a, b) => a.height - b.height),
placeItems: items => {
let y = 0;
items.forEach(item => {
item.x = 0;
item.y = y;
y += item.height;
});
return items;
}
},
'left-right': {
sort: items => items.toSorted((a, b) => a.width - b.width),
placeItems: items => {
var x = 0;
items.forEach(item => {
item.x = x;
item.y = 0;
x += item.width;
});
return items;
}
},
diagonal: {
sort: items => items.toSorted((a, b) => {
const aDiag = Math.sqrt(Math.pow(a.height, 2) + Math.pow(a.width, 2));
const bDiag = Math.sqrt(Math.pow(b.height, 2) + Math.pow(b.width, 2));
return aDiag - bDiag;
}),
placeItems: items => {
let x = 0;
let y = 0;
items.forEach(item => {
item.x = x;
item.y = y;
x += item.width;
y += item.height;
});
}
},
'alt-diagonal': {
sort: items => items.toSorted((a, b) => {
const aDiag = Math.sqrt(Math.pow(a.height, 2) + Math.pow(a.width, 2));
const bDiag = Math.sqrt(Math.pow(b.height, 2) + Math.pow(b.width, 2));
return aDiag - bDiag;
}),
placeItems: items => {
let x = 0;
let y = 0;
items.forEach(item => {
item.x = x - item.width;
item.y = y;
x += item.width;
y += item.height;
});
}
},
'binary-tree': {
sort: items => items,
placeItems: items => {
binpack(items, {inPlace: true});
return items
}
}
};
class PackingSmith {
constructor(algorithm, options) {
this.items = [];
this.algorithm = algorithm;
options = options || {};
var sort = options.sort !== undefined ? options.sort : true;
this.sort = sort;
}
addItem(item) {
this.items.push(item);
}
normalizeCoordinates() {
var items = this.items;
var minX = Infinity;
var minY = Infinity;
items.forEach(item => {
var coords = item;
minX = Math.min(minX, coords.x);
minY = Math.min(minY, coords.y);
});
items.forEach(item => {
var coords = item;
coords.x -= minX;
coords.y -= minY;
});
}
getStats() {
const { x, y } = this.items.reduce((acc, item) => {
acc.x.min.push(item.x);
acc.y.min.push(item.y);
acc.x.max.push(item.x + item.width);
acc.y.max.push(item.y + item.height);
return acc;
}, {
x: { min: [], max: [] },
y: { min: [], max: [] }
});
return {
minX: Math.max(...x.min),
maxX: Math.max(...x.max),
maxY: Math.max(...y.max),
minY: Math.max(...y.min)
}
}
getItems() {
return this.items;
}
processItems() {
var items = this.items;
if (this.sort) {
items = this.algorithm.sort(items);
}
items = this.algorithm.placeItems(items);
this.items = items;
return items;
}
exportItems() {
this.processItems();
this.normalizeCoordinates();
return this.items;
}
export() {
var items = this.exportItems();
var stats = this.getStats();
var retObj = {
'height': stats.maxY,
'width': stats.maxX,
'items': items
};
return retObj;
}
}
function Layout(algorithmName, options) {
var algorithm = algorithmName || 'top-down';
if (typeof algorithm === 'string') {
algorithm = algorithms[algorithmName];
}
var retSmith = new PackingSmith(algorithm, options);
return retSmith;
}
Layout.PackingSmith = PackingSmith;
function addAlgorithm(name, algorithm) {
algorithms[name] = algorithm;
}
Layout.addAlgorithm = addAlgorithm;
Layout.algorithms = algorithms;
return Layout;
}));