free-tex-packer-core
Version:
Free texture packer core
248 lines (193 loc) • 8.04 kB
JavaScript
let MaxRectsBinPack = require('./packers/MaxRectsBin');
let OptimalPacker = require('./packers/OptimalPacker');
let allPackers = require('./packers').list;
let Trimmer = require('./utils/Trimmer');
let TextureRenderer = require('./utils/TextureRenderer');
class PackProcessor {
static detectIdentical(rects) {
let identical = [];
for (let i = 0; i < rects.length; i++) {
let rect1 = rects[i];
for (let n = i + 1; n < rects.length; n++) {
let rect2 = rects[n];
if (rect1.image._base64 == rect2.image._base64 && identical.indexOf(rect2) < 0) {
rect2.identical = rect1;
identical.push(rect2);
}
}
}
for (let rect of identical) {
rects.splice(rects.indexOf(rect), 1);
}
return {
rects: rects,
identical: identical
}
}
static applyIdentical(rects, identical) {
let clones = [];
let removeIdentical = [];
for (let item of identical) {
let ix = rects.indexOf(item.identical);
if (ix >= 0) {
let rect = rects[ix];
let clone = Object.assign({}, rect);
clone.name = item.name;
clone.image = item.image;
clone.skipRender = true;
removeIdentical.push(item);
clones.push(clone);
}
}
for (let item of removeIdentical) {
identical.splice(identical.indexOf(item), 1);
}
for (let item of clones) {
item.cloned = true;
rects.push(item);
}
return rects;
}
static pack(images = {}, options = {}, onComplete = null, onError = null) {
let rects = [];
let padding = options.padding || 0;
let extrude = options.extrude || 0;
let maxWidth = 0, maxHeight = 0;
let minWidth = 0, minHeight = 0;
let alphaThreshold = options.alphaThreshold || 0;
if (alphaThreshold > 255) alphaThreshold = 255;
let names = Object.keys(images).sort();
for (let key of names) {
let img = images[key];
maxWidth += img.width;
maxHeight += img.height;
if (img.width > minWidth) minWidth = img.width + padding * 2 + extrude * 2;
if (img.height > minHeight) minHeight = img.height + padding * 2 + extrude * 2;
rects.push({
frame: { x: 0, y: 0, w: img.width, h: img.height },
rotated: false,
trimmed: false,
spriteSourceSize: { x: 0, y: 0, w: img.width, h: img.height },
sourceSize: { w: img.width, h: img.height },
name: key,
image: img
});
}
let width = options.width || 0;
let height = options.height || 0;
if (!width) width = maxWidth;
if (!height) height = maxHeight;
if (options.powerOfTwo) {
let sw = Math.round(Math.log(width) / Math.log(2));
let sh = Math.round(Math.log(height) / Math.log(2));
let pw = Math.pow(2, sw);
let ph = Math.pow(2, sh);
if (pw < width) pw = Math.pow(2, sw + 1);
if (ph < height) ph = Math.pow(2, sh + 1);
width = pw;
height = ph;
}
if (width < minWidth || height < minHeight) {
if (onError) onError({
description: "Invalid size. Min: " + minWidth + "x" + minHeight
});
return;
}
if (options.allowTrim) {
Trimmer.trim(rects, alphaThreshold);
}
for (let item of rects) {
item.frame.w += padding * 2 + extrude * 2;
item.frame.h += padding * 2 + extrude * 2;
}
let identical = [];
if (options.detectIdentical) {
let res = PackProcessor.detectIdentical(rects);
rects = res.rects;
identical = res.identical;
}
let getAllPackers = () => {
let methods = [];
for (let packerClass of allPackers) {
if (packerClass !== OptimalPacker) {
for (let method in packerClass.methods) {
methods.push({ packerClass, packerMethod: packerClass.methods[method], allowRotation: false });
if (options.allowRotation) {
methods.push({ packerClass, packerMethod: packerClass.methods[method], allowRotation: true });
}
}
}
}
return methods;
};
let packerClass = options.packer || MaxRectsBinPack;
let packerMethod = options.packerMethod || MaxRectsBinPack.methods.BestShortSideFit;
let packerCombos = (packerClass === OptimalPacker) ? getAllPackers() : [{ packerClass, packerMethod, allowRotation: options.allowRotation }];
let optimalRes;
let optimalSheets = Infinity;
let optimalEfficiency = 0;
let sourceArea = 0;
for (let rect of rects) {
sourceArea += rect.sourceSize.w * rect.sourceSize.h;
}
for (let combo of packerCombos) {
let res = [];
let sheetArea = 0;
// duplicate rects if more than 1 combo since the array is mutated in pack()
let _rects = packerCombos.length > 1 ? rects.map(rect => {
return Object.assign({}, rect, {
frame: Object.assign({}, rect.frame),
spriteSourceSize: Object.assign({}, rect.spriteSourceSize),
sourceSize: Object.assign({}, rect.sourceSize)
});
}) : rects;
// duplicate identical if more than 1 combo and fix references to point to the
// cloned rects since the array is mutated in applyIdentical()
let _identical = packerCombos.length > 1 ? identical.map(rect => {
for (let rect2 of _rects) {
if (rect.identical.image._base64 == rect2.image._base64) {
return Object.assign({}, rect, { identical: rect2 });
}
}
}) : identical;
while (_rects.length) {
let packer = new combo.packerClass(width, height, combo.allowRotation);
let result = packer.pack(_rects, combo.packerMethod);
for (let item of result) {
item.frame.x += padding + extrude;
item.frame.y += padding + extrude;
item.frame.w -= padding * 2 + extrude * 2;
item.frame.h -= padding * 2 + extrude * 2;
}
if (options.detectIdentical) {
result = PackProcessor.applyIdentical(result, _identical);
}
res.push(result);
for (let item of result) {
this.removeRect(_rects, item.name);
}
let { width: sheetWidth, height: sheetHeight } = TextureRenderer.getSize(result, options);
sheetArea += sheetWidth * sheetHeight;
}
let sheets = res.length;
let efficiency = sourceArea / sheetArea;
if (sheets < optimalSheets || (sheets === optimalSheets && efficiency > optimalEfficiency)) {
optimalRes = res;
optimalSheets = sheets;
optimalEfficiency = efficiency;
}
}
if (onComplete) {
onComplete(optimalRes);
}
}
static removeRect(rects, name) {
for (let i = 0; i < rects.length; i++) {
if (rects[i].name == name) {
rects.splice(i, 1);
return;
}
}
}
}
module.exports = PackProcessor;