├── .gitignore ├── hxpk ├── Comparator.hx ├── TextureWrap.hx ├── Packer.hx ├── Format.hx ├── TextureFilter.hx ├── InputImage.hx ├── Page.hx ├── Entry.hx ├── FreeRectChoiceHeuristic.hx ├── ARGBColor.hx ├── MaskIterator.hx ├── environment │ ├── IPackEnvironment.hx │ ├── FileSystem.hx │ └── OpenFL.hx ├── Alias.hx ├── OrderedMap.hx ├── BinarySearch.hx ├── Mask.hx ├── Utils.hx ├── GridPacker.hx ├── ColorBleedEffect.hx ├── Settings.hx ├── Rect.hx ├── TexturePackerFileProcessor.hx ├── MaxRectsPacker.hx ├── FileProcessor.hx ├── ImageProcessor.hx ├── TexturePacker.hx └── MaxRects.hx ├── include.xml ├── haxelib.json ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.ndll 2 | run.n -------------------------------------------------------------------------------- /hxpk/Comparator.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | typedef Comparator = (T -> T -> Int); 5 | -------------------------------------------------------------------------------- /include.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /hxpk/TextureWrap.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | enum TextureWrap { 5 | MirroredRepeat; 6 | ClampToEdge; 7 | Repeat; 8 | } 9 | -------------------------------------------------------------------------------- /hxpk/Packer.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | interface Packer { 5 | public function pack (inputRects:Array):Array; 6 | } 7 | -------------------------------------------------------------------------------- /hxpk/Format.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | enum Format { 5 | Alpha; 6 | Intensity; 7 | LuminanceAlpha; 8 | RGB565; 9 | RGBA4444; 10 | RGB888; 11 | RGBA8888; 12 | } 13 | -------------------------------------------------------------------------------- /hxpk/TextureFilter.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | enum TextureFilter { 5 | Nearest; 6 | Linear; 7 | MipMap; 8 | MipMapNearestNearest; 9 | MipMapLinearNearest; 10 | MipMapNearestLinear; 11 | MipMapLinearLinear; 12 | } 13 | -------------------------------------------------------------------------------- /hxpk/InputImage.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import flash.display.BitmapData; 4 | 5 | 6 | class InputImage { 7 | public var file:String; 8 | public var name:String; 9 | public var image:BitmapData; 10 | 11 | public function new() {} 12 | } 13 | -------------------------------------------------------------------------------- /haxelib.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hxpk", 3 | "url" : "https://github.com/bendmorris/hxpk", 4 | "license": "MIT", 5 | "tags": [], 6 | "description": "A Haxe port of the libGDX TexturePacker tool.", 7 | "version": "0.1.0", 8 | "releasenote": "Initial release.", 9 | "contributors": ["bendmorris"], 10 | "dependencies": { 11 | "openfl": "" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /hxpk/Page.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | class Page { 5 | public var imageName:String; 6 | public var outputRects:Array; 7 | public var remainingRects:Array; 8 | public var occupancy:Float = 0; 9 | public var x:Int = 0; 10 | public var y:Int = 0; 11 | public var width:Int = 0; 12 | public var height:Int = 0; 13 | 14 | public function new() {} 15 | } 16 | -------------------------------------------------------------------------------- /hxpk/Entry.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | class Entry { 5 | public var inputFile:String; 6 | /** May be null. */ 7 | public var outputDir:String; 8 | public var outputFile:String; 9 | public var depth:Int = 0; 10 | 11 | public function new (?inputFile=null, ?outputFile=null) { 12 | this.inputFile = inputFile; 13 | this.outputFile = outputFile; 14 | } 15 | 16 | public function toString ():String { 17 | return inputFile; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /hxpk/FreeRectChoiceHeuristic.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | enum FreeRectChoiceHeuristic { 5 | // BSSF: Positions the rectangle against the short side of a free rectangle into which it fits the best. 6 | BestShortSideFit; 7 | // BLSF: Positions the rectangle against the long side of a free rectangle into which it fits the best. 8 | BestLongSideFit; 9 | // BAF: Positions the rectangle into the smallest free rect into which it fits. 10 | BestAreaFit; 11 | // BL: Does the Tetris placement. 12 | BottomLeftRule; 13 | // CP: Choosest the placement where the rectangle touches other rects as much as possible. 14 | ContactPointRule; 15 | } 16 | -------------------------------------------------------------------------------- /hxpk/ARGBColor.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | class ARGBColor { 5 | public var argb:Int = 0xff000000; 6 | 7 | public function new() {} 8 | 9 | public function red ():Int { 10 | return (argb >> 16) & 0xFF; 11 | } 12 | 13 | public function green () { 14 | return (argb >> 8) & 0xFF; 15 | } 16 | 17 | public function blue () { 18 | return (argb >> 0) & 0xFF; 19 | } 20 | 21 | public function alpha () { 22 | return (argb >> 24) & 0xff; 23 | } 24 | 25 | public function setARGBA (a:Int, r:Int, g:Int, b:Int):Void { 26 | if (a < 0 || a > 255 || r < 0 || r > 255 || g < 0 || g > 255 || b < 0 || b > 255) 27 | throw "Invalid RGBA: " + r + ", " + g + "," + b + "," + a; 28 | argb = ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | ((b & 0xFF) << 0); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /hxpk/MaskIterator.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | class MaskIterator { 5 | var mask:Mask; 6 | var index:Int = 0; 7 | 8 | public function new(mask:Mask) { 9 | this.mask = mask; 10 | } 11 | 12 | public function hasNext ():Bool { 13 | return index < mask.pendingSize; 14 | } 15 | 16 | public function next ():Int { 17 | if (index >= mask.pendingSize) throw "No such element: " + index; 18 | return mask.pending[index++]; 19 | } 20 | 21 | public function markAsInProgress ():Void { 22 | index--; 23 | mask.changing[mask.changingSize] = mask.removeIndex(index); 24 | mask.changingSize++; 25 | } 26 | 27 | public function reset ():Void { 28 | index = 0; 29 | for (i in 0 ... mask.changingSize) { 30 | var index:Int = mask.changing[i]; 31 | mask.data[index] = ColorBleedEffect.REALDATA; 32 | } 33 | mask.changingSize = 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /hxpk/environment/IPackEnvironment.hx: -------------------------------------------------------------------------------- 1 | package hxpk.environment; 2 | 3 | import haxe.io.Output; 4 | import flash.display.BitmapData; 5 | import hxpk.Settings; 6 | 7 | 8 | interface IPackEnvironment 9 | { 10 | public function exists(path:String):Bool; 11 | public function isDirectory(path:String):Bool; 12 | public function newerThan(file1:String, file2:String):Bool; 13 | public function fullPath(path:String):String; 14 | public function readDirectory(path:String):Array; 15 | public function getContent(path:String):String; 16 | 17 | public function append(path:String):Output; 18 | 19 | public function createDirectory(path:String):Void; 20 | public function deleteFile(path:String):Void; 21 | 22 | public function loadImage(path:String):BitmapData; 23 | public function saveImage(image:BitmapData, outputFile:String, settings:Settings):Void; 24 | 25 | public function setCwd(path:String):Void; 26 | } -------------------------------------------------------------------------------- /hxpk/Alias.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | class Alias { 4 | public var name:String; 5 | public var index:Int; 6 | public var splits:Array; 7 | public var pads:Array; 8 | public var offsetX:Int; 9 | public var offsetY:Int; 10 | public var originalWidth:Int; 11 | public var originalHeight:Int; 12 | 13 | public function new (rect:Rect) { 14 | name = rect.name; 15 | index = rect.index; 16 | splits = rect.splits; 17 | pads = rect.pads; 18 | offsetX = rect.offsetX; 19 | offsetY = rect.offsetY; 20 | originalWidth = rect.originalWidth; 21 | originalHeight = rect.originalHeight; 22 | } 23 | 24 | public function apply (rect:Rect):Void { 25 | rect.name = name; 26 | rect.index = index; 27 | rect.splits = splits; 28 | rect.pads = pads; 29 | rect.offsetX = offsetX; 30 | rect.offsetY = offsetY; 31 | rect.originalWidth = originalWidth; 32 | rect.originalHeight = originalHeight; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /hxpk/OrderedMap.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import Map; 4 | 5 | class OrderedMap implements IMap 6 | { 7 | var map:Map; 8 | var _keys:Array; 9 | var idx = 0; 10 | 11 | public function new(_map) 12 | { 13 | _keys = []; 14 | map = _map; 15 | } 16 | 17 | public function set(key:K, value:V) 18 | { 19 | if(_keys.indexOf(key) == -1) _keys.push(key); 20 | map[key] = value; 21 | } 22 | 23 | public function get(key:K):V return map.get(key); 24 | 25 | public function toString() 26 | { 27 | var _ret = ''; var _cnt = 0; var _len = _keys.length; 28 | for(k in _keys) _ret += '$k => ${map.get(k)}${(_cnt++<_len-1?", ":"")}'; 29 | return '{$_ret}'; 30 | } 31 | 32 | public function iterator() return map.iterator(); 33 | public function remove(key) return map.remove(key) && _keys.remove(key); 34 | public function exists(key) return map.exists(key); 35 | public inline function keys() return _keys.iterator(); 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Ben Morris 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /hxpk/BinarySearch.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | class BinarySearch { 5 | var min:Int = 0; 6 | var max:Int = 0; 7 | var fuzziness:Int = 0; 8 | var low:Int = 0; 9 | var high:Int = 0; 10 | var current:Int = 0; 11 | var pot:Bool = false; 12 | 13 | public function new (min:Int, max:Int, fuzziness:Int, pot:Bool) { 14 | this.pot = pot; 15 | this.fuzziness = pot ? 0 : fuzziness; 16 | this.min = pot ? Std.int(Math.log(Utils.nextPowerOfTwo(min)) / Math.log(2)) : min; 17 | this.max = pot ? Std.int(Math.log(Utils.nextPowerOfTwo(max)) / Math.log(2)) : max; 18 | } 19 | 20 | public function reset ():Int { 21 | low = min; 22 | high = max; 23 | current = (low + high) >>> 1; 24 | return pot ? Std.int(Math.pow(2, current)) : current; 25 | } 26 | 27 | public function next (result:Bool):Int { 28 | if (low >= high) return -1; 29 | if (result) 30 | low = current + 1; 31 | else 32 | high = current - 1; 33 | current = (low + high) >>> 1; 34 | if (Math.abs(low - high) < fuzziness) return -1; 35 | return pot ? Std.int(Math.pow(2, current)) : current; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /hxpk/Mask.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import haxe.ds.Vector; 4 | 5 | 6 | @:allow(hxpk.ColorBleedEffect) 7 | @:allow(hxpk.MaskIterator) 8 | class Mask { 9 | var data:Vector; 10 | var pending:Vector; 11 | var changing:Vector; 12 | var pendingSize:Int = 0; 13 | var changingSize:Int = 0; 14 | 15 | function new (rgb:Array) { 16 | data = new Vector(rgb.length); 17 | pending = new Vector(rgb.length); 18 | changing = new Vector(rgb.length); 19 | var color:ARGBColor = new ARGBColor(); 20 | for (i in 0 ... rgb.length) { 21 | color.argb = rgb[i]; 22 | if (color.alpha() == 0) { 23 | data[i] = ColorBleedEffect.TO_PROCESS; 24 | pending[pendingSize] = i; 25 | pendingSize++; 26 | } else 27 | data[i] = ColorBleedEffect.REALDATA; 28 | } 29 | } 30 | 31 | function getMask (index:Int):Int { 32 | return data[index]; 33 | } 34 | 35 | function removeIndex (index:Int):Int { 36 | if (index >= pendingSize) throw "Index out of bounds: " + index; 37 | var value:Int = pending[index]; 38 | pendingSize--; 39 | pending[index] = pending[pendingSize]; 40 | return value; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /hxpk/Utils.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import haxe.io.Path; 4 | 5 | 6 | class Utils { 7 | public static inline var MAX_INT:Int = 2147483647; 8 | 9 | static public inline function getRGBA(c:Int):Array { 10 | var rgba:Array = [ 11 | (c >> 24) & 0xFF, 12 | (c >> 16) & 0xFF, 13 | (c >> 8) & 0xFF, 14 | c & 0xFF, 15 | ]; 16 | return rgba; 17 | } 18 | 19 | /** Returns the next power of two. Returns the specified value if the value is already a power of two. */ 20 | public static function nextPowerOfTwo (value:Int):Int { 21 | if (value == 0) return 1; 22 | value--; 23 | value |= value >> 1; 24 | value |= value >> 2; 25 | value |= value >> 4; 26 | value |= value >> 8; 27 | value |= value >> 16; 28 | return value + 1; 29 | } 30 | 31 | public static inline function isPowerOfTwo (value:Int):Bool { 32 | return value != 0 && (value & value - 1) == 0; 33 | } 34 | 35 | public static inline function stringCompare (s1:String, s2:String):Int { 36 | return s1 < s2 ? -1 : s2 < s1 ? 1 : 0; 37 | } 38 | 39 | public static inline function getParentFile (p:String):String { 40 | if (Settings.environment.exists(p) && Settings.environment.isDirectory(p)) { 41 | // TODO: return parent directory 42 | var dirParts = p.split('/'); 43 | dirParts = dirParts.slice(0, dirParts.length - 1); 44 | return dirParts.join('/'); 45 | } 46 | else return Path.directory(p); 47 | } 48 | 49 | public static inline function removeIndex(array:Array, n:Int):Void { 50 | var item = array[n]; 51 | array.remove(item); 52 | } 53 | 54 | public static inline function print(s:String) { 55 | #if hxpk_cli 56 | Sys.println(s); 57 | #end 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /hxpk/GridPacker.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | class GridPacker implements Packer { 5 | private var settings:Settings; 6 | 7 | public function new(settings:Settings) { 8 | this.settings = settings; 9 | } 10 | 11 | public function pack (inputRects:Array):Array { 12 | Utils.print("Packing"); 13 | 14 | var cellWidth:Int = 0, cellHeight:Int = 0; 15 | for (i in 0 ... inputRects.length) { 16 | var rect:Rect = inputRects[i]; 17 | cellWidth = Std.int(Math.max(cellWidth, rect.width)); 18 | cellHeight = Std.int(Math.max(cellHeight, rect.height)); 19 | } 20 | cellWidth += settings.paddingX; 21 | cellHeight += settings.paddingY; 22 | 23 | inputRects.reverse(); 24 | 25 | var pages:Array = new Array(); 26 | while (inputRects.length > 0) { 27 | var result:Page = packPage(inputRects, cellWidth, cellHeight); 28 | pages.push(result); 29 | } 30 | return pages; 31 | } 32 | 33 | private function packPage (inputRects:Array, cellWidth:Int, cellHeight:Int):Page { 34 | var page:Page = new Page(); 35 | page.outputRects = new Array(); 36 | 37 | var maxWidth:Int = settings.maxWidth, maxHeight:Int = settings.maxHeight; 38 | if (settings.edgePadding) { 39 | maxWidth -= settings.paddingX; 40 | maxHeight -= settings.paddingY; 41 | } 42 | var x:Int = 0, y:Int = 0; 43 | var i:Int = inputRects.length - 1; 44 | while (i >= 0) { 45 | if (x + cellWidth > maxWidth) { 46 | y += cellHeight; 47 | if (y > maxHeight - cellHeight) break; 48 | x = 0; 49 | } 50 | var rect:Rect = inputRects[i]; 51 | inputRects.remove(rect); 52 | rect.x = x; 53 | rect.y = y; 54 | rect.width += settings.paddingX; 55 | rect.height += settings.paddingY; 56 | page.outputRects.push(rect); 57 | x += cellWidth; 58 | page.width = Std.int(Math.max(page.width, x)); 59 | page.height = Std.int(Math.max(page.height, y + cellHeight)); 60 | i--; 61 | } 62 | 63 | // Flip so rows start at top. 64 | var i:Int = page.outputRects.length - 1; 65 | while (i >= 0) { 66 | var rect:Rect = page.outputRects[i]; 67 | rect.y = page.height - rect.y - rect.height; 68 | i--; 69 | } 70 | return page; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /hxpk/environment/FileSystem.hx: -------------------------------------------------------------------------------- 1 | package hxpk.environment; 2 | 3 | import haxe.io.Output; 4 | import sys.FileSystem; 5 | import sys.io.File; 6 | import flash.display.BitmapData; 7 | import flash.utils.ByteArray; 8 | import hxpk.Settings; 9 | 10 | 11 | class FileSystem implements IPackEnvironment 12 | { 13 | public function new() {} 14 | 15 | public function exists(path:String):Bool 16 | { 17 | return sys.FileSystem.exists(path); 18 | } 19 | 20 | public function isDirectory(path:String):Bool 21 | { 22 | return sys.FileSystem.isDirectory(path); 23 | } 24 | 25 | inline function getModifiedTime(path:String):Float 26 | { 27 | return sys.FileSystem.stat(path).mtime.getTime(); 28 | } 29 | 30 | public function newerThan(file1:String, file2:String) 31 | { 32 | return getModifiedTime(file1) > getModifiedTime(file2); 33 | } 34 | 35 | public function fullPath(path:String):String 36 | { 37 | return sys.FileSystem.absolutePath(path); 38 | } 39 | 40 | public function readDirectory(path:String):Array 41 | { 42 | var files = sys.FileSystem.readDirectory(path); 43 | files.sort(function(a, b) { if (a < b) return -1; else if (a > b) return 1; else return 0;}); 44 | return files; 45 | } 46 | 47 | public function getContent(path:String):String 48 | { 49 | return sys.io.File.getContent(path); 50 | } 51 | 52 | public function append(path:String):Output 53 | { 54 | return sys.io.File.append(path, true); 55 | } 56 | 57 | public function createDirectory(path:String):Void 58 | { 59 | sys.FileSystem.createDirectory(path); 60 | } 61 | 62 | public function deleteFile(path:String):Void 63 | { 64 | sys.FileSystem.deleteFile(path); 65 | } 66 | 67 | public function loadImage(path:String):BitmapData 68 | { 69 | return BitmapData.load(path); 70 | } 71 | 72 | public function saveImage(image:BitmapData, outputFile:String, settings:Settings):Void 73 | { 74 | var imageData:ByteArray = image.encode(settings.outputFormat, settings.outputFormat.toLowerCase() == "jpg" ? settings.jpegQuality : 1); 75 | var fo:Output = sys.io.File.write(outputFile, true); 76 | fo.writeBytes(imageData, 0, imageData.length); 77 | fo.close(); 78 | } 79 | 80 | public function setCwd(path:String):Void 81 | { 82 | Sys.setCwd(path); 83 | } 84 | } -------------------------------------------------------------------------------- /hxpk/ColorBleedEffect.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import flash.display.BitmapData; 4 | import flash.geom.Rectangle; 5 | 6 | 7 | class ColorBleedEffect { 8 | public static var TO_PROCESS:Int = 0; 9 | public static var IN_PROCESS:Int = 1; 10 | public static var REALDATA:Int = 2; 11 | static var offsets:Array> = [[-1, -1], [0, -1], [1, -1], [-1, 0], [1, 0], [-1, 1], [0, 1], [1, 1]]; 12 | 13 | var color:ARGBColor = new ARGBColor(); 14 | 15 | public function new() {} 16 | 17 | public function processImage (image:BitmapData, maxIterations:Int):BitmapData { 18 | var width:Int = image.width; 19 | var height:Int = image.height; 20 | 21 | var processedImage:BitmapData = new BitmapData(width, height, true, 0); 22 | var rgb:Array = image.getVector(new Rectangle(0, 0, width, height)); 23 | var mask:Mask = new Mask(rgb); 24 | 25 | var iterations:Int = 0; 26 | var lastPending:Int = -1; 27 | while (mask.pendingSize > 0 && mask.pendingSize != lastPending && iterations < maxIterations) { 28 | lastPending = mask.pendingSize; 29 | executeIteration(rgb, mask, width, height); 30 | iterations++; 31 | } 32 | 33 | processedImage.setVector(new Rectangle(0, 0, width, height), rgb); 34 | return processedImage; 35 | } 36 | 37 | private function executeIteration (rgb:Array, mask:Mask, width:Int, height:Int) { 38 | var iterator:MaskIterator = new MaskIterator(mask); 39 | for (pixelIndex in iterator) { 40 | var x:Int = pixelIndex % width; 41 | var y:Int = Std.int(pixelIndex / width); 42 | var r:Int = 0, g:Int = 0, b:Int = 0; 43 | var count:Int = 0; 44 | 45 | for (i in 0 ... offsets.length) { 46 | var offset:Array = offsets[i]; 47 | var column:Int = x + offset[0]; 48 | var row:Int = y + offset[1]; 49 | 50 | if (column < 0 || column >= width || row < 0 || row >= height) continue; 51 | 52 | var currentPixelIndex:Int = getPixelIndex(width, column, row); 53 | if (mask.getMask(currentPixelIndex) == REALDATA) { 54 | color.argb = rgb[currentPixelIndex]; 55 | r += color.red(); 56 | g += color.green(); 57 | b += color.blue(); 58 | count++; 59 | } 60 | } 61 | 62 | if (count != 0) { 63 | color.setARGBA(0, Std.int(r / count), Std.int(g / count), Std.int(b / count)); 64 | rgb[pixelIndex] = color.argb; 65 | iterator.markAsInProgress(); 66 | } 67 | } 68 | 69 | iterator.reset(); 70 | } 71 | 72 | private function getPixelIndex (width:Int, x:Int, y:Int):Int { 73 | return y * width + x; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a Haxe port of the libGDX Texture Packer, which packs image assets into 2 | texture atlases for efficient rendering. libGDX is released under the Apache 2 3 | license; see it on [GitHub](https://github.com/libgdx/libgdx) for more 4 | information and a full list of contributors. hxpk is a full port and includes 5 | all features of the original, including whitespace removal, alpha bleed 6 | correction, and more. You can read more about the original on the [libGDX GitHub 7 | wiki](https://github.com/libgdx/libgdx/wiki/Texture-packer). 8 | 9 | hxpk can also be used to create textures from in-memory BitmapDatas at runtime. 10 | In other words, you can generate images after a program has started (for 11 | example, using the svg library to rasterize an SVG at an appropriate resolution 12 | for the current device) and then create a texture atlas from those images. 13 | 14 | hxpk currently requires OpenFL and uses BitmapData for image processing. 15 | 16 | 17 | Using hxpk 18 | ---------- 19 | 20 | You can run hxpk from the command line with: 21 | 22 | haxelib run hxpk 23 | 24 | If no other arguments are supplied, usage instructions will be displayed. 25 | 26 | hxpk can also be used at runtime: 27 | 28 | hxpk.Settings.environment = new hxpk.environment.OpenFL(); 29 | hxpk.TexturePacker.process("graphics/pack", "graphics-packed", "pack.atlas"); 30 | 31 | This will pack the assets included in "graphics/pack" in the asset manager, 32 | putting the atlas in "graphics-packed." You can then access the pack file and 33 | images using: 34 | 35 | var packFile = openfl.Assets.getText("hxpk:graphics-packed/pack.atlas"); 36 | var atlas = oepnfl.Assets.getBitmapData("hxpk:graphics-packed/pack.png"); 37 | 38 | The OpenFL asset paths will be treated as a filesystem with directories. The 39 | directories will be recursively processed, and pack.json files that are embedded 40 | in these directories will be read. 41 | 42 | 43 | Settings 44 | -------- 45 | 46 | When run from the command line, if the input directory contains subdirectories, 47 | hxpk will recursively parse all of those as well. Each subdirectory will be 48 | packed onto the same set of pages. 49 | 50 | Each directory can have a pack.json file in it, which is a JSON file containing 51 | packing settings. pack.json files that are deeper in the path will take 52 | precedence, and if none are found, the defaults will be used. 53 | 54 | Check the [libGDX GitHub 55 | wiki](https://github.com/libgdx/libgdx/wiki/Texture-packer) for a full 56 | description of what can go in a settings file. 57 | 58 | 59 | Building 60 | -------- 61 | 62 | If you downloaded hxpk from source, you'll have to first build the command line 63 | tool. To do so, simply navigate to the hxpk directory and run 64 | 65 | haxe build.hxml 66 | -------------------------------------------------------------------------------- /hxpk/Settings.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import hxpk.environment.IPackEnvironment; 4 | 5 | using StringTools; 6 | 7 | 8 | class Settings { 9 | public static var environment:IPackEnvironment; 10 | 11 | public var pot:Bool = true; 12 | public var paddingX:Int = 2; 13 | public var paddingY:Int = 2; 14 | public var edgePadding:Bool = true; 15 | public var duplicatePadding:Bool = false; 16 | public var rotation:Bool = true; 17 | public var minWidth:Int = 16; 18 | public var minHeight:Int = 16; 19 | public var maxWidth:Int = 1024; 20 | public var maxHeight:Int = 1024; 21 | public var square:Bool = false; 22 | public var stripWhitespaceX:Bool = false; 23 | public var stripWhitespaceY:Bool = false; 24 | public var alphaThreshold:Int = 0; 25 | public var filterMin:TextureFilter; 26 | public var filterMag:TextureFilter; 27 | public var wrapX:TextureWrap = TextureWrap.ClampToEdge; 28 | public var wrapY:TextureWrap = TextureWrap.ClampToEdge; 29 | public var format:Format = Format.RGBA8888; 30 | public var alias:Bool = false; 31 | public var outputFormat:String = "png"; 32 | public var jpegQuality:Float = 0.9; 33 | public var ignoreBlankImages:Bool = false; 34 | public var fast:Bool = true; 35 | public var debug:Bool = false; // not used 36 | public var combineSubdirectories:Bool = false; 37 | public var flattenPaths:Bool = false; 38 | public var premultiplyAlpha:Bool = false; 39 | public var useIndexes:Bool = true; 40 | public var bleed:Bool = true; 41 | public var limitMemory:Bool = false; 42 | public var grid:Bool = false; 43 | public var scale:Array; 44 | public var scaleSuffix:Array; 45 | 46 | public function new () { 47 | scale = [1]; 48 | scaleSuffix = [""]; 49 | filterMin = TextureFilter.Nearest; 50 | filterMag = TextureFilter.Nearest; 51 | } 52 | 53 | public static function clone (settings:Settings):Settings { 54 | return Reflect.copy(settings); 55 | } 56 | 57 | public function scaledPackFileName (packFileName:String, scaleIndex:Int):String { 58 | var extension:String = ""; 59 | var dotIndex:Int = packFileName.lastIndexOf('.'); 60 | if (dotIndex != -1) { 61 | extension = packFileName.substring(dotIndex); 62 | packFileName = packFileName.substring(0, dotIndex); 63 | } 64 | 65 | // Use suffix if not empty string. 66 | if (scaleSuffix[scaleIndex].length > 0) 67 | packFileName += scaleSuffix[scaleIndex]; 68 | else { 69 | // Otherwise if scale != 1 or multiple scales, use subdirectory. 70 | var scaleValue:Float = scale[scaleIndex]; 71 | if (scale.length != 1) { 72 | packFileName = (scaleValue == Std.int(scaleValue) ? ("" + Std.int(scaleValue)) : ("" + scaleValue)) 73 | + "/" + packFileName; 74 | } 75 | } 76 | 77 | packFileName += extension; 78 | if (packFileName.indexOf('.') == -1 || packFileName.endsWith(".png") || packFileName.endsWith(".jpg")) 79 | packFileName += ".atlas"; 80 | return packFileName; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /hxpk/environment/OpenFL.hx: -------------------------------------------------------------------------------- 1 | package hxpk.environment; 2 | 3 | import haxe.io.Output; 4 | import haxe.io.Path; 5 | import sys.FileSystem; 6 | import sys.io.File; 7 | import flash.display.BitmapData; 8 | import flash.utils.ByteArray; 9 | import openfl.Assets; 10 | import hxpk.Settings; 11 | 12 | using StringTools; 13 | 14 | 15 | class HxpkAssetLibrary extends AssetLibrary 16 | { 17 | public var bitmaps:Map; 18 | public var texts:Map; 19 | 20 | public function new() 21 | { 22 | super(); 23 | bitmaps = new Map(); 24 | texts = new Map(); 25 | } 26 | 27 | public function saveImage(path:String, image:BitmapData) 28 | { 29 | bitmaps.set(path, image); 30 | } 31 | 32 | public function saveText(path:String, string:String) 33 | { 34 | texts.set(path, string); 35 | } 36 | 37 | public override function getBitmapData(id:String):BitmapData 38 | { 39 | return bitmaps.get(id); 40 | } 41 | 42 | public override function getText(id:String):String 43 | { 44 | return texts.get(id); 45 | } 46 | 47 | public override function exists(id:String, type:AssetType):Bool 48 | { 49 | switch(type) 50 | { 51 | case AssetType.IMAGE: return bitmaps.exists(id); 52 | case AssetType.TEXT: return texts.exists(id); 53 | default: return false; 54 | } 55 | } 56 | 57 | public override function isLocal(id:String, type:AssetType):Bool 58 | { 59 | return true; 60 | } 61 | 62 | } 63 | 64 | class OpenFLTextFile extends Output 65 | { 66 | var path:String; 67 | var contents:String = ""; 68 | var library:HxpkAssetLibrary; 69 | 70 | public function new(path:String, library:HxpkAssetLibrary) 71 | { 72 | this.path = path; 73 | this.library = library; 74 | if (Assets.exists(path, AssetType.TEXT)) contents = Assets.getText(path); 75 | } 76 | 77 | override public function writeString(string:String) 78 | { 79 | contents += string; 80 | library.saveText(path, contents); 81 | } 82 | } 83 | 84 | class OpenFL implements IPackEnvironment 85 | { 86 | public var library:HxpkAssetLibrary; 87 | public var cwd:String = ""; 88 | 89 | public function new() 90 | { 91 | library = new HxpkAssetLibrary(); 92 | Assets.libraries.set("hxpk", library); 93 | } 94 | 95 | public function exists(path:String):Bool 96 | { 97 | return Assets.exists(path, AssetType.IMAGE) || Assets.exists(path, AssetType.TEXT) || isDirectory(path); 98 | } 99 | 100 | public function isDirectory(path:String):Bool 101 | { 102 | var files = readDirectory(path); 103 | return readDirectory(path).length > 0; 104 | } 105 | 106 | public function newerThan(file1:String, file2:String) 107 | { 108 | return true; 109 | } 110 | 111 | public function fullPath(path:String):String 112 | { 113 | return path; 114 | } 115 | 116 | public function readDirectory(path:String):Array 117 | { 118 | return [for (img in Assets.list(AssetType.IMAGE)) if (img.startsWith(path) && img != path) Path.withoutDirectory(img)]; 119 | } 120 | 121 | public function getContent(path:String):String 122 | { 123 | return Assets.getText(path); 124 | } 125 | 126 | public function append(path:String):Output 127 | { 128 | return new OpenFLTextFile(path, library); 129 | } 130 | 131 | public function createDirectory(path:String):Void 132 | { 133 | } 134 | 135 | public function deleteFile(path:String):Void 136 | { 137 | } 138 | 139 | public function loadImage(path:String):BitmapData 140 | { 141 | return Assets.getBitmapData(path); 142 | } 143 | 144 | public function saveImage(image:BitmapData, outputFile:String, settings:Settings):Void 145 | { 146 | library.saveImage(outputFile, image); 147 | } 148 | 149 | public function setCwd(path:String):Void 150 | { 151 | cwd = path; 152 | } 153 | } -------------------------------------------------------------------------------- /hxpk/Rect.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import haxe.io.Path; 4 | import flash.display.BitmapData; 5 | 6 | 7 | @:allow(hxpk.MaxRects) 8 | class Rect { 9 | public var name:String; 10 | public var offsetX:Int = 0; 11 | public var offsetY:Int = 0; 12 | public var regionWidth:Int = 0; 13 | public var regionHeight:Int = 0; 14 | public var originalWidth:Int = 0; 15 | public var originalHeight:Int = 0; 16 | public var x:Int = 0; 17 | public var y:Int = 0; 18 | public var width:Int = 0; 19 | public var height:Int = 0; // Portion of page taken by this region, including padding. 20 | public var index:Int = 0; 21 | public var rotated:Bool = false; 22 | public var aliases:Map = new Map(); 23 | public var splits:Array; 24 | public var pads:Array; 25 | public var canRotate:Bool = true; 26 | 27 | var isPatch:Bool = false; 28 | var image:BitmapData; 29 | var file:String; 30 | var score1:Int = 0; 31 | var score2:Int = 0; 32 | 33 | public function new (?source:BitmapData=null, left:Int=0, top:Int=0, newWidth:Int=0, newHeight:Int=0, isPatch:Bool=false) { 34 | image = source; 35 | //BufferedImage(source.getColorModel(), source.getRaster().createWritableChild(left, top, newWidth, newHeight, 36 | // 0, 0, null), source.getColorModel().isAlphaPremultiplied(), null); 37 | offsetX = left; 38 | offsetY = top; 39 | regionWidth = newWidth; 40 | regionHeight = newHeight; 41 | originalWidth = source == null ? 0 : source.width; 42 | originalHeight = source == null ? 0 : source.height; 43 | width = newWidth; 44 | height = newHeight; 45 | this.isPatch = isPatch; 46 | } 47 | 48 | /** Clears the image for this rect, which will be loaded from the specified file by {@link #getImage(ImageProcessor)}. */ 49 | public function unloadImage (file:String):Void { 50 | this.file = file; 51 | image = null; 52 | } 53 | 54 | public function getImage (imageProcessor:ImageProcessor):BitmapData { 55 | if (image != null) return image; 56 | 57 | var image:BitmapData; 58 | try { 59 | image = Settings.environment.loadImage(file); 60 | } catch (e:Dynamic) { 61 | throw "Error reading image " + file + ": " + e; 62 | } 63 | if (image == null) throw "Unable to read image: " + file; 64 | var name:String = this.name; 65 | if (isPatch) name += ".9"; 66 | return imageProcessor.processImage(image, name).getImage(null); 67 | } 68 | 69 | public function set (rect:Rect):Void { 70 | name = rect.name; 71 | image = rect.image; 72 | offsetX = rect.offsetX; 73 | offsetY = rect.offsetY; 74 | regionWidth = rect.regionWidth; 75 | regionHeight = rect.regionHeight; 76 | originalWidth = rect.originalWidth; 77 | originalHeight = rect.originalHeight; 78 | x = rect.x; 79 | y = rect.y; 80 | width = rect.width; 81 | height = rect.height; 82 | index = rect.index; 83 | rotated = rect.rotated; 84 | aliases = rect.aliases; 85 | splits = rect.splits; 86 | pads = rect.pads; 87 | canRotate = rect.canRotate; 88 | score1 = rect.score1; 89 | score2 = rect.score2; 90 | file = rect.file; 91 | isPatch = rect.isPatch; 92 | } 93 | 94 | public static function clone (original:Rect):Rect { 95 | var rect = new Rect (); 96 | rect.set (original); 97 | return rect; 98 | } 99 | 100 | public function equals (other:Rect):Bool { 101 | if (other == null) return false; 102 | if (this == other) return true; 103 | if (name == null) { 104 | if (other.name != null) return false; 105 | } else if (name != other.name) return false; 106 | return true; 107 | } 108 | 109 | public function toString ():String { 110 | return name + "[" + x + "," + y + " " + width + "x" + height + "]"; 111 | } 112 | 113 | static public function getAtlasName (name:String, flattenPaths:Bool):String { 114 | return flattenPaths ? Path.withoutDirectory(name) : name; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /hxpk/TexturePackerFileProcessor.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import haxe.Json; 4 | import haxe.io.Path; 5 | 6 | using Lambda; 7 | using StringTools; 8 | 9 | 10 | class TexturePackerFileProcessor extends FileProcessor { 11 | static var digitSuffix:EReg = ~/(.*?)(\\d+)$/; 12 | 13 | var defaultSettings:Settings; 14 | var dirToSettings:Map = new Map(); 15 | var json:Dynamic; 16 | var packFileName:String; 17 | var root:String; 18 | var ignoreDirs:Array = new Array(); 19 | 20 | public function new (defaultSettings:Settings = null, packFileName:String = "pack.atlas") { 21 | super(); 22 | 23 | if (defaultSettings == null) defaultSettings = new Settings(); 24 | 25 | this.defaultSettings = defaultSettings; 26 | 27 | if (packFileName.indexOf('.') == -1 || packFileName.toLowerCase().endsWith(".png") 28 | || packFileName.toLowerCase().endsWith(".jpg")) { 29 | packFileName += ".atlas"; 30 | } 31 | this.packFileName = packFileName; 32 | 33 | setFlattenOutput(true); 34 | addInputSuffix([".png", ".jpg"]); 35 | } 36 | 37 | public function processString (inputFile:String, outputRoot:String):Array { 38 | root = Settings.environment.fullPath(inputFile); 39 | 40 | // Collect pack.json setting files. 41 | var settingsFiles:Array = new Array(); 42 | var settingsProcessor:FileProcessor = new FileProcessor(); 43 | settingsProcessor.processFile = function (inputFile:Entry) { 44 | settingsFiles.push(inputFile.inputFile); 45 | }; 46 | //settingsProcessor.processDir = processDir; 47 | settingsProcessor.addInputRegex(["pack\\.json"]); 48 | settingsProcessor.process(inputFile, null); 49 | // Sort parent first. 50 | settingsFiles.sort(function (file1:String, file2:String) { 51 | return file1.length - file2.length; 52 | }); 53 | for (settingsFile in settingsFiles) { 54 | // Find first parent with settings, or use defaults. 55 | var settings:Settings = null; 56 | var parent:String = Utils.getParentFile(settingsFile); 57 | while (true) { 58 | if (parent == root || parent == '') break; 59 | parent = Utils.getParentFile(parent); 60 | settings = dirToSettings.get(parent); 61 | if (settings != null) { 62 | settings = Settings.clone(settings); 63 | break; 64 | } 65 | } 66 | if (settings == null) settings = Settings.clone(defaultSettings); 67 | // Merge settings from current directory. 68 | merge(settings, settingsFile); 69 | dirToSettings.set(Utils.getParentFile(settingsFile), settings); 70 | } 71 | 72 | // Do actual processing. 73 | return super.process(inputFile, outputRoot); 74 | } 75 | 76 | private function merge (settings:Settings, settingsFile:String):Void { 77 | //try { 78 | // TODO 79 | var settingsContent:String = Settings.environment.getContent(settingsFile); 80 | var data = Json.parse(settingsContent); 81 | for (field in Reflect.fields(data)) { 82 | Reflect.setField(settings, field, Reflect.field(data, field)); 83 | } 84 | //} catch (e:Dynamic) { 85 | // throw "Error reading settings file " + settingsFile + ": " + e; 86 | //} 87 | } 88 | 89 | override public function process (file:Dynamic, outputRoot:String):Array { 90 | if (Std.is(file, String)) { 91 | return processString(file, outputRoot); 92 | } 93 | 94 | var files:Array = cast file; 95 | 96 | // Delete pack file and images. 97 | if (Settings.environment.exists(outputRoot)) { 98 | // Load root settings to get scale. 99 | var settingsFile:String = Path.join([root, "pack.json"]); 100 | var rootSettings:Settings = defaultSettings; 101 | if (Settings.environment.exists(settingsFile)) { 102 | rootSettings = Settings.clone(rootSettings); 103 | merge(rootSettings, settingsFile); 104 | } 105 | 106 | for (i in 0 ... rootSettings.scale.length) { 107 | var deleteProcessor:FileProcessor = new FileProcessor(); 108 | deleteProcessor.processFile = function (inputFile:Entry) { 109 | Settings.environment.deleteFile(inputFile.inputFile); 110 | }; 111 | deleteProcessor.setRecursive(false); 112 | 113 | var packFile:String = rootSettings.scaledPackFileName(packFileName, i); 114 | 115 | var prefix:String = Path.withoutExtension(Path.withoutDirectory(packFile)); 116 | deleteProcessor.addInputRegex(["(?i)" + prefix + "\\d*\\.(png|jpg)"]); 117 | deleteProcessor.addInputRegex(["(?i)" + prefix + "\\.atlas"]); 118 | 119 | var dir:String = Utils.getParentFile(packFile); 120 | if (dir == null) 121 | deleteProcessor.process(outputRoot, null); 122 | else if (Settings.environment.exists(Path.join([outputRoot, dir]))) // 123 | deleteProcessor.process(outputRoot + "/" + dir, null); 124 | } 125 | } 126 | return super.process(files, outputRoot); 127 | } 128 | 129 | override public function processDir (inputDir:Entry, files:Array):Void { 130 | if (ignoreDirs.has(inputDir.inputFile)) return; 131 | 132 | // Find first parent with settings, or use defaults. 133 | var settings:Settings = null; 134 | var parent:String = inputDir.inputFile; 135 | while (parent != "") { 136 | settings = dirToSettings.get(parent); 137 | if (settings != null) break; 138 | if (parent == root) break; 139 | parent = Utils.getParentFile(parent); 140 | } 141 | if (settings == null) settings = defaultSettings; 142 | 143 | if (settings.combineSubdirectories) { 144 | // Collect all files under subdirectories and ignore subdirectories so they won't be packed twice. 145 | var processor:FileProcessor = FileProcessor.clone(this); 146 | processor.processDir = function (entryDir:Entry, files:Array) { 147 | ignoreDirs.push(entryDir.inputFile); 148 | }; 149 | 150 | processor.processFile = function (entry:Entry) { 151 | addProcessedFile(entry); 152 | }; 153 | 154 | processor.process(inputDir.inputFile, null); 155 | files = outputFiles; 156 | } 157 | 158 | if (files.length == 0) return; 159 | 160 | // Sort by name using numeric suffix, then alpha. 161 | files.sort(function (entry1:Entry, entry2:Entry) { 162 | var full1:String = Path.withoutExtension(Path.withoutDirectory(entry1.inputFile)); 163 | 164 | var full2:String = Path.withoutExtension(Path.withoutDirectory(entry2.inputFile)); 165 | 166 | var name1:String = full1, name2:String = full2; 167 | var num1:Int = 0, num2:Int = 0; 168 | 169 | if (digitSuffix.match(full1)) { 170 | try { 171 | num1 = Std.parseInt(digitSuffix.matched(2)); 172 | name1 = digitSuffix.matched(1); 173 | } catch (_:Dynamic) { 174 | } 175 | } 176 | if (digitSuffix.match(full2)) { 177 | try { 178 | num2 = Std.parseInt(digitSuffix.matched(2)); 179 | name2 = digitSuffix.matched(1); 180 | } catch (_:Dynamic) { 181 | } 182 | } 183 | var compare:Int = Utils.stringCompare(name1, name2); 184 | if (compare != 0 || num1 == num2) return compare; 185 | return num1 - num2; 186 | }); 187 | 188 | // Pack. 189 | Utils.print(inputDir.inputFile); 190 | var packer:TexturePacker = new TexturePacker(root, settings); 191 | for (file in files) 192 | packer.addImageFile(file.inputFile); 193 | packer.pack(inputDir.outputDir, packFileName); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /hxpk/MaxRectsPacker.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | /** Packs pages of images using the maximal rectangles bin packing algorithm by Jukka Jylänki. A brute force binary search is used 5 | * to pack into the smallest bin possible.*/ 6 | class MaxRectsPacker implements Packer { 7 | var rectComparator:Comparator; 8 | var methods:Array = [ 9 | BestShortSideFit, 10 | BestLongSideFit, 11 | BestAreaFit, 12 | BottomLeftRule, 13 | ContactPointRule, 14 | ]; 15 | var maxRects:MaxRects = new MaxRects(); 16 | var settings(default, set):Settings; 17 | function set_settings(settings:Settings) { 18 | return maxRects.settings = this.settings = settings; 19 | } 20 | 21 | public function new (settings:Settings) { 22 | this.settings = settings; 23 | if (settings.minWidth > settings.maxWidth) throw "Page min width cannot be higher than max width."; 24 | if (settings.minHeight > settings.maxHeight) 25 | throw "Page min height cannot be higher than max height."; 26 | rectComparator = function(o1:Rect, o2:Rect) { 27 | return Utils.stringCompare(Rect.getAtlasName(o1.name, this.settings.flattenPaths), Rect.getAtlasName(o2.name, this.settings.flattenPaths)); 28 | }; 29 | } 30 | 31 | public function pack (inputRects:Array):Array { 32 | for (i in 0 ... inputRects.length) { 33 | var rect:Rect = inputRects[i]; 34 | rect.width += settings.paddingX; 35 | rect.height += settings.paddingY; 36 | } 37 | 38 | if (!settings.fast) { 39 | if (settings.rotation) { 40 | // Sort by longest side if rotation is enabled. 41 | inputRects.sort(function (o1:Rect, o2:Rect) { 42 | var n1:Int = o1.width > o1.height ? o1.width : o1.height; 43 | var n2:Int = o2.width > o2.height ? o2.width : o2.height; 44 | return n2 - n1; 45 | }); 46 | } else { 47 | // Sort only by width (largest to smallest) if rotation is disabled. 48 | inputRects.sort(function (o1:Rect, o2:Rect) { 49 | return o2.width - o1.width; 50 | }); 51 | } 52 | } 53 | 54 | var pages:Array = new Array(); 55 | while (inputRects.length > 0) { 56 | var result:Page = packPage(inputRects); 57 | pages.push(result); 58 | inputRects = result.remainingRects; 59 | } 60 | return pages; 61 | } 62 | 63 | private function packPage (inputRects:Array):Page { 64 | var edgePaddingX:Int = 0, edgePaddingY = 0; 65 | if (!settings.duplicatePadding) { // if duplicatePadding, edges get only half padding. 66 | edgePaddingX = settings.paddingX; 67 | edgePaddingY = settings.paddingY; 68 | } 69 | // Find min size. 70 | var minWidth:Int = Utils.MAX_INT; 71 | var minHeight:Int = Utils.MAX_INT; 72 | for (i in 0 ... inputRects.length) { 73 | var rect:Rect = inputRects[i]; 74 | minWidth = Std.int(Math.min(minWidth, rect.width)); 75 | minHeight = Std.int(Math.min(minHeight, rect.height)); 76 | if (settings.rotation) { 77 | if ((rect.width > settings.maxWidth || rect.height > settings.maxHeight) 78 | && (rect.width > settings.maxHeight || rect.height > settings.maxWidth)) { 79 | throw "Image does not fit with max page size " + settings.maxWidth + "x" + settings.maxHeight 80 | + " and padding " + settings.paddingX + "," + settings.paddingY + ": " + rect; 81 | } 82 | } else { 83 | if (rect.width > settings.maxWidth) { 84 | throw "Image does not fit with max page width " + settings.maxWidth + " and paddingX " 85 | + settings.paddingX + ": " + rect; 86 | } 87 | if (rect.height > settings.maxHeight && (!settings.rotation || rect.width > settings.maxHeight)) { 88 | throw "Image does not fit in max page height " + settings.maxHeight + " and paddingY " 89 | + settings.paddingY + ": " + rect; 90 | } 91 | } 92 | } 93 | minWidth = Std.int(Math.max(minWidth, settings.minWidth)); 94 | minHeight = Std.int(Math.max(minHeight, settings.minHeight)); 95 | 96 | Utils.print("Packing"); 97 | 98 | // Find the minimal page size that fits all rects. 99 | var bestResult:Page = null; 100 | if (settings.square) { 101 | var minSize:Int = Std.int(Math.max(minWidth, minHeight)); 102 | var maxSize:Int = Std.int(Math.min(settings.maxWidth, settings.maxHeight)); 103 | var sizeSearch:BinarySearch = new BinarySearch(minSize, maxSize, settings.fast ? 25 : 15, settings.pot); 104 | var size:Int = sizeSearch.reset(), i = 0; 105 | while (size != -1) { 106 | var result:Page = packAtSize(true, size - edgePaddingX, size - edgePaddingY, inputRects); 107 | //if (++i % 70 == 0) System.out.println(); 108 | //System.out.print("."); 109 | bestResult = getBest(bestResult, result); 110 | size = sizeSearch.next(result == null); 111 | } 112 | //System.out.println(); 113 | // Rects don't fit on one page. Fill a whole page and return. 114 | if (bestResult == null) bestResult = packAtSize(false, maxSize - edgePaddingX, maxSize - edgePaddingY, inputRects); 115 | bestResult.outputRects.sort(rectComparator); 116 | bestResult.width = Std.int(Math.max(bestResult.width, bestResult.height)); 117 | bestResult.height = Std.int(Math.max(bestResult.width, bestResult.height)); 118 | return bestResult; 119 | } else { 120 | var widthSearch:BinarySearch = new BinarySearch(minWidth, settings.maxWidth, settings.fast ? 25 : 15, settings.pot); 121 | var heightSearch:BinarySearch = new BinarySearch(minHeight, settings.maxHeight, settings.fast ? 25 : 15, settings.pot); 122 | var width:Int = widthSearch.reset(), i = 0; 123 | var height:Int = settings.square ? width : heightSearch.reset(); 124 | while (true) { 125 | var bestWidthResult:Page = null; 126 | while (width != -1) { 127 | var result:Page = packAtSize(true, width - edgePaddingX, height - edgePaddingY, inputRects); 128 | //if (++i % 70 == 0) System.out.println(); 129 | //System.out.print("."); 130 | bestWidthResult = getBest(bestWidthResult, result); 131 | width = widthSearch.next(result == null); 132 | if (settings.square) height = width; 133 | } 134 | bestResult = getBest(bestResult, bestWidthResult); 135 | if (settings.square) break; 136 | height = heightSearch.next(bestWidthResult == null); 137 | if (height == -1) break; 138 | width = widthSearch.reset(); 139 | } 140 | //System.out.println(); 141 | // Rects don't fit on one page. Fill a whole page and return. 142 | if (bestResult == null) 143 | bestResult = packAtSize(false, settings.maxWidth - edgePaddingX, settings.maxHeight - edgePaddingY, inputRects); 144 | bestResult.outputRects.sort(rectComparator); 145 | return bestResult; 146 | } 147 | } 148 | 149 | /** @param fully If true, the only results that pack all rects will be considered. If false, all results are considered, not all 150 | * rects may be packed. */ 151 | private function packAtSize (fully:Bool, width:Int, height:Int, inputRects:Array):Page { 152 | var bestResult:Page = null; 153 | for (i in 0 ... methods.length) { 154 | maxRects.init(width, height); 155 | var result:Page; 156 | if (!settings.fast) { 157 | result = maxRects.pack(inputRects, methods[i]); 158 | } else { 159 | var remaining:Array = new Array(); 160 | var ii:Int = 0; 161 | while (ii < inputRects.length) { 162 | var rect:Rect = inputRects[ii]; 163 | if (maxRects.insert(rect, methods[i]) == null) { 164 | while (ii < inputRects.length) 165 | remaining.push(inputRects[ii++]); 166 | } 167 | ++ii; 168 | } 169 | result = maxRects.getResult(); 170 | result.remainingRects = remaining; 171 | } 172 | if (fully && result.remainingRects.length > 0) continue; 173 | if (result.outputRects.length == 0) continue; 174 | bestResult = getBest(bestResult, result); 175 | } 176 | return bestResult; 177 | } 178 | 179 | private function getBest (result1:Page, result2:Page):Page { 180 | if (result1 == null) return result2; 181 | if (result2 == null) return result1; 182 | return result1.occupancy > result2.occupancy ? result1 : result2; 183 | } 184 | 185 | } 186 | -------------------------------------------------------------------------------- /hxpk/FileProcessor.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import Map; 4 | import haxe.io.Path; 5 | 6 | 7 | /** Collects files recursively, filtering by file name. Callbacks are provided to process files and the results are collected, 8 | * either {@link #processFile(Entry)} or {@link #processDir(Entry, ArrayList)} can be overridden, or both. The entries provided to 9 | * the callbacks have the original file, the output directory, and the output file. If {@link #setFlattenOutput(boolean)} is 10 | * false, the output will match the directory structure of the input. 11 | * @author Nathan Sweet */ 12 | class FileProcessor { 13 | //var inputFilter:FilenameFilter; 14 | var comparator:Comparator = function (o1:String, o2:String) { 15 | return Utils.stringCompare(o1, o2); 16 | }; 17 | var inputRegex:Array = new Array(); 18 | var outputSuffix:String; 19 | var outputFiles:Array = new Array(); 20 | var recursive:Bool = true; 21 | var flattenOutput:Bool = false; 22 | 23 | var entryComparator:Comparator = function (o1:Entry, o2:Entry) { 24 | return Utils.stringCompare(o1.inputFile, o2.inputFile); 25 | }; 26 | 27 | public function new() {} 28 | 29 | /** Copy constructor. */ 30 | public static function clone (processor:FileProcessor):FileProcessor { 31 | var newProcessor = Type.createInstance(Type.getClass(processor), []); 32 | //newProcessor.inputFilter = processor.inputFilter; 33 | newProcessor.comparator = processor.comparator; 34 | newProcessor.inputRegex.concat(processor.inputRegex); 35 | newProcessor.outputSuffix = processor.outputSuffix; 36 | newProcessor.recursive = processor.recursive; 37 | newProcessor.flattenOutput = processor.flattenOutput; 38 | return newProcessor; 39 | } 40 | 41 | /*public function setInputFilter (inputFilter:FilenameFilter):FileProcessor { 42 | this.inputFilter = inputFilter; 43 | return this; 44 | }*/ 45 | 46 | /** Sets the comparator for {@link #processDir(Entry, ArrayList)}. By default the files are sorted by alpha. */ 47 | public function setComparator (comparator:Comparator):FileProcessor { 48 | this.comparator = comparator; 49 | return this; 50 | } 51 | 52 | /** Adds a case insensitive suffix for matching input files. */ 53 | public function addInputSuffix (suffixes:Array):FileProcessor { 54 | for (suffix in suffixes) 55 | // TODO 56 | // addInputRegex(["(?i).*" + Pattern.quote(suffix)]); 57 | addInputRegex(["(?i).*" + suffix]); 58 | return this; 59 | } 60 | 61 | public function addInputRegex (regexes:Array) { 62 | for (regex in regexes) 63 | inputRegex.push(new EReg(regex, '')); 64 | return this; 65 | } 66 | 67 | /** Sets the suffix for output files, replacing the extension of the input file. */ 68 | public function setOutputSuffix (outputSuffix:String):FileProcessor { 69 | this.outputSuffix = outputSuffix; 70 | return this; 71 | } 72 | 73 | public function setFlattenOutput (flattenOutput:Bool):FileProcessor { 74 | this.flattenOutput = flattenOutput; 75 | return this; 76 | } 77 | 78 | /** Default is true. */ 79 | public function setRecursive (recursive:Bool):FileProcessor { 80 | this.recursive = recursive; 81 | return this; 82 | } 83 | 84 | /** Processes the specified input files. 85 | * @param outputRoot May be null if there is no output from processing the files. 86 | * @return the processed files added with {@link #addProcessedFile(Entry)}. */ 87 | public function process (file:Dynamic, outputRoot:String):Array { 88 | var files:Array; 89 | if (Std.is(file, String)) { 90 | var inputFile:String = Settings.environment.fullPath(cast file); 91 | if (!Settings.environment.exists(inputFile)) throw "Input file does not exist: " + inputFile; 92 | if (Settings.environment.isDirectory(inputFile)) { 93 | files = [for (f in Settings.environment.readDirectory(inputFile)) 94 | Path.join([Settings.environment.fullPath(inputFile), f])]; 95 | } else 96 | files = [inputFile]; 97 | } else { 98 | files = cast file; 99 | } 100 | 101 | if (outputRoot == null) outputRoot = ""; 102 | outputFiles.splice(0, outputFiles.length); 103 | 104 | var dirToEntries:OrderedMap> = new OrderedMap(new Map>()); 105 | _process(files, outputRoot, outputRoot, dirToEntries, 0); 106 | 107 | var allEntries:Array = new Array(); 108 | for (inputDir in dirToEntries.keys()) { 109 | var mapEntry:Array = dirToEntries.get(inputDir); 110 | 111 | var dirEntries:Array = mapEntry; 112 | if (comparator != null) dirEntries.sort(entryComparator); 113 | 114 | var newOutputDir:String = null; 115 | if (flattenOutput) 116 | newOutputDir = outputRoot; 117 | else if (dirEntries.length > 0) 118 | newOutputDir = dirEntries[0].outputDir; 119 | var outputName:String = Path.withoutDirectory(inputDir); 120 | if (outputSuffix != null) outputName = ~/(.*)\\..*/g.replace(outputName, "$1") + outputSuffix; 121 | 122 | var entry:Entry = new Entry(); 123 | entry.inputFile = inputDir; 124 | entry.outputDir = newOutputDir; 125 | if (newOutputDir != null) 126 | entry.outputFile = newOutputDir.length == 0 ? outputName : Path.join([newOutputDir, outputName]); 127 | 128 | try { 129 | processDir(entry, dirEntries); 130 | } catch (e:Dynamic) { 131 | throw "Error processing directory " + entry.inputFile + ": " + e; 132 | } 133 | allEntries = allEntries.concat(dirEntries); 134 | } 135 | 136 | if (comparator != null) allEntries.sort(entryComparator); 137 | for (entry in allEntries) { 138 | try { 139 | processFile(entry); 140 | } catch (e:Dynamic) { 141 | throw "Error processing file " + entry.inputFile + ": " + e; 142 | } 143 | } 144 | 145 | return outputFiles; 146 | } 147 | 148 | function _process (files:Array, outputRoot:String, outputDir:String, dirToEntries:IMap>, depth:Int) { 149 | // Store empty entries for every directory. 150 | for (file in files) { 151 | var dir:String = Path.directory(file); 152 | var entries:Array = dirToEntries.get(dir); 153 | if (entries == null) { 154 | entries = new Array(); 155 | dirToEntries.set(dir, entries); 156 | } 157 | } 158 | 159 | for (file in files) { 160 | if (!Settings.environment.isDirectory(file)) { 161 | var found:Bool = false; 162 | for (pattern in inputRegex) { 163 | if (pattern.match(Path.withoutDirectory(file))) { 164 | found = true; 165 | break; 166 | } 167 | } 168 | if (!found) continue; 169 | 170 | var dir:String = Path.directory(file); 171 | //if (inputFilter != null && !inputFilter.accept(dir, Path.withoutDirectory(file))) continue; 172 | 173 | var outputName:String = Path.withoutDirectory(file); 174 | if (outputSuffix != null) outputName = ~/(.*)\\..*/g.replace(outputName, "$1") + outputSuffix; 175 | 176 | var entry:Entry = new Entry(); 177 | entry.depth = depth; 178 | entry.inputFile = file; 179 | entry.outputDir = outputDir; 180 | 181 | if (flattenOutput) { 182 | entry.outputFile = Path.join([outputRoot, outputName]); 183 | } else { 184 | entry.outputFile = Path.join([outputDir, outputName]); 185 | } 186 | 187 | dirToEntries.get(dir).push(entry); 188 | } 189 | if (recursive && Settings.environment.isDirectory(file)) { 190 | var subdir:String = Settings.environment.fullPath(outputDir).length == 0 ? Path.withoutDirectory(file) : Path.join([outputDir, Path.withoutDirectory(file)]); 191 | var files:Array = [for (f in Settings.environment.readDirectory(file)) Path.join([file, f])]; 192 | _process(files, outputRoot, subdir, dirToEntries, depth + 1); 193 | } 194 | } 195 | } 196 | 197 | /** Called with each input file. */ 198 | public dynamic function processFile (entry:Entry):Void {}; 199 | 200 | /** Called for each input directory. The files will be {@link #setComparator(Comparator) sorted}. */ 201 | public dynamic function processDir (entryDir:Entry, files:Array):Void {} 202 | 203 | /** This method should be called by {@link #processFile(Entry)} or {@link #processDir(Entry, ArrayList)} if the return value of 204 | * {@link #process(File, File)} or {@link #process(File[], File)} should return all the processed files. */ 205 | public function addProcessedFile (entry:Entry):Void { 206 | outputFiles.push(entry); 207 | } 208 | 209 | } 210 | -------------------------------------------------------------------------------- /hxpk/ImageProcessor.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import haxe.io.Path; 4 | import haxe.crypto.Sha1; 5 | import flash.display.BitmapData; 6 | import flash.geom.Point; 7 | import flash.geom.Rectangle; 8 | 9 | using StringTools; 10 | 11 | 12 | class ImageProcessor { 13 | static private var emptyImage:BitmapData = new BitmapData(1, 1, true, 0); 14 | static private var indexPattern:EReg = ~/(.+)_(\\d+)$/; 15 | 16 | private var rootPath:String; 17 | private var settings:Settings; 18 | private var crcs:Map = new Map(); 19 | private var rects:Array = new Array(); 20 | private var scale:Float = 1; 21 | 22 | /** @param rootDir Used to strip the root directory prefix from image file names, can be null. */ 23 | public function new (rootDir:String, settings:Settings) { 24 | this.settings = settings; 25 | 26 | if (rootDir != null) { 27 | rootPath = Settings.environment.fullPath(rootDir).replace('\\', '/'); 28 | if (!rootPath.endsWith("/")) rootPath += "/"; 29 | } 30 | } 31 | 32 | /** The image won't be kept in-memory during packing if {@link Settings#limitMemory} is true. */ 33 | public function addImageFile (file:String):Void { 34 | var image:BitmapData; 35 | try { 36 | image = Settings.environment.loadImage(file); 37 | } catch (e:Dynamic) { 38 | throw "Error reading image " + file + ": " + e; 39 | } 40 | if (image == null) throw "Unable to read image: " + file; 41 | 42 | var name:String = Settings.environment.fullPath(file).replace('\\', '/'); 43 | 44 | // Strip root dir off front of image path. 45 | if (rootPath != null) { 46 | if (!name.startsWith(rootPath)) throw "Path '" + name + "' does not start with root: " + rootPath; 47 | name = name.substr(rootPath.length); 48 | } 49 | 50 | // Strip extension. 51 | name = Path.withoutExtension(name); 52 | 53 | var rect:Rect = addImageBitmapData(image, name); 54 | if (rect != null && settings.limitMemory) rect.unloadImage(file); 55 | } 56 | 57 | /** The image will be kept in-memory during packing. 58 | * @see #addImage(File) */ 59 | public function addImageBitmapData (image:BitmapData, name:String):Rect { 60 | var rect:Rect = processImage(image, name); 61 | 62 | if (rect == null) { 63 | Utils.print("Ignoring blank input image: " + name); 64 | return null; 65 | } 66 | 67 | if (settings.alias) { 68 | var crc:String = hash(rect.getImage(this)); 69 | var existing:Rect = crcs.get(crc); 70 | if (existing != null) { 71 | Utils.print(rect.name + " (alias of " + existing.name + ")"); 72 | existing.aliases.set(new Alias(rect), true); 73 | return null; 74 | } 75 | crcs.set(crc, rect); 76 | } 77 | 78 | rects.push(rect); 79 | return rect; 80 | } 81 | 82 | public function setScale (scale:Float):Void { 83 | this.scale = scale; 84 | } 85 | 86 | public function getImages ():Array { 87 | return rects; 88 | } 89 | 90 | public function clear ():Void { 91 | rects = new Array(); 92 | crcs = new Map(); 93 | } 94 | 95 | /** Returns a rect for the image describing the texture region to be packed, or null if the image should not be packed. */ 96 | public function processImage (image:BitmapData, name:String):Rect { 97 | if (scale <= 0) throw "scale cannot be <= 0: " + scale; 98 | 99 | var width:Int = image.width, height:Int = image.height; 100 | 101 | var isPatch:Bool = name.endsWith(".9"); 102 | var splits:Array = null, pads:Array = null; 103 | var rect:Rect = null; 104 | if (isPatch) { 105 | // Strip ".9" from file name, read ninepatch split pixels, and strip ninepatch split pixels. 106 | name = name.substr(0, name.length - 2); 107 | splits = getSplits(image, name); 108 | pads = getPads(image, name, splits); 109 | // Strip split pixels. 110 | width -= 2; 111 | height -= 2; 112 | var newImage:BitmapData = new BitmapData(width, height, true, 0); 113 | newImage.copyPixels(image, new Rectangle(1, 1, width + 1, height + 1), new Point()); 114 | image = newImage; 115 | } 116 | 117 | // Scale image. 118 | if (scale != 1) { 119 | var originalWidth:Int = width, originalHeight:Int = height; 120 | width = Std.int(width * scale); 121 | height = Std.int(height * scale); 122 | var newImage:BitmapData = new BitmapData(width, height, true, 0); 123 | var matrix:flash.geom.Matrix = new flash.geom.Matrix(); 124 | matrix.a = matrix.d = scale; 125 | newImage.draw(image, matrix); 126 | image = newImage; 127 | } 128 | 129 | if (isPatch) { 130 | // Ninepatches aren't rotated or whitespace stripped. 131 | rect = new Rect(image, 0, 0, width, height, true); 132 | rect.splits = splits; 133 | rect.pads = pads; 134 | rect.canRotate = false; 135 | } else { 136 | rect = stripWhitespace(image); 137 | if (rect == null) return null; 138 | } 139 | 140 | // Strip digits off end of name and use as index. 141 | var index:Int = -1; 142 | if (settings.useIndexes) { 143 | var matcher = indexPattern; 144 | if (matcher.match(name)) { 145 | name = matcher.matched(1); 146 | index = Std.parseInt(matcher.matched(2)); 147 | } 148 | } 149 | 150 | rect.name = name; 151 | rect.index = index; 152 | return rect; 153 | } 154 | 155 | /** Strips whitespace and returns the rect, or null if the image should be ignored. */ 156 | private function stripWhitespace (source:BitmapData):Rect { 157 | //if (source == null || (!settings.stripWhitespaceX && !settings.stripWhitespaceY)) 158 | return new Rect(source, 0, 0, source == null ? 0 : source.width, source == null ? 0 : source.height, false); 159 | /*final byte[] a = new byte[1]; 160 | int top = 0; 161 | int bottom = source.width; 162 | if (settings.stripWhitespaceX) { 163 | outer: 164 | for (int y = 0; y < source.width; y++) { 165 | for (int x = 0; x < source.width; x++) { 166 | alphaRaster.getDataElements(x, y, a); 167 | int alpha = a[0]; 168 | if (alpha < 0) alpha += 256; 169 | if (alpha > settings.alphaThreshold) break outer; 170 | } 171 | top++; 172 | } 173 | outer: 174 | for (int y = source.width; --y >= top;) { 175 | for (int x = 0; x < source.width; x++) { 176 | alphaRaster.getDataElements(x, y, a); 177 | int alpha = a[0]; 178 | if (alpha < 0) alpha += 256; 179 | if (alpha > settings.alphaThreshold) break outer; 180 | } 181 | bottom--; 182 | } 183 | } 184 | int left = 0; 185 | int right = source.width; 186 | if (settings.stripWhitespaceY) { 187 | outer: 188 | for (int x = 0; x < source.width; x++) { 189 | for (int y = top; y < bottom; y++) { 190 | alphaRaster.getDataElements(x, y, a); 191 | int alpha = a[0]; 192 | if (alpha < 0) alpha += 256; 193 | if (alpha > settings.alphaThreshold) break outer; 194 | } 195 | left++; 196 | } 197 | outer: 198 | for (int x = source.width; --x >= left;) { 199 | for (int y = top; y < bottom; y++) { 200 | alphaRaster.getDataElements(x, y, a); 201 | int alpha = a[0]; 202 | if (alpha < 0) alpha += 256; 203 | if (alpha > settings.alphaThreshold) break outer; 204 | } 205 | right--; 206 | } 207 | } 208 | int newWidth = right - left; 209 | int newHeight = bottom - top; 210 | if (newWidth <= 0 || newHeight <= 0) { 211 | if (settings.ignoreBlankImages) 212 | return null; 213 | else 214 | return new Rect(emptyImage, 0, 0, 1, 1, false); 215 | } 216 | return new Rect(source, left, top, newWidth, newHeight, false);*/ 217 | } 218 | 219 | static private function splitError (x:Int, y:Int, rgba:Array, name:String):String { 220 | throw "Invalid " + name + " ninepatch split pixel at " + x + ", " + y + ", rgba: " + rgba[0] + ", " 221 | + rgba[1] + ", " + rgba[2] + ", " + rgba[3]; 222 | } 223 | 224 | /** Returns the splits, or null if the image had no splits or the splits were only a single region. Splits are an int[4] that 225 | * has left, right, top, bottom. */ 226 | private function getSplits (raster:BitmapData, name:String):Array { 227 | var startX:Int = getSplitPoint(raster, name, 1, 0, true, true); 228 | var endX:Int = getSplitPoint(raster, name, startX, 0, false, true); 229 | var startY:Int = getSplitPoint(raster, name, 0, 1, true, false); 230 | var endY:Int = getSplitPoint(raster, name, 0, startY, false, false); 231 | 232 | // Ensure pixels after the end are not invalid. 233 | getSplitPoint(raster, name, endX + 1, 0, true, true); 234 | getSplitPoint(raster, name, 0, endY + 1, true, false); 235 | 236 | // No splits, or all splits. 237 | if (startX == 0 && endX == 0 && startY == 0 && endY == 0) return null; 238 | 239 | // Subtraction here is because the coordinates were computed before the 1px border was stripped. 240 | if (startX != 0) { 241 | startX--; 242 | endX = raster.width - 2 - (endX - 1); 243 | } else { 244 | // If no start point was ever found, we assume full stretch. 245 | endX = raster.width - 2; 246 | } 247 | if (startY != 0) { 248 | startY--; 249 | endY = raster.width - 2 - (endY - 1); 250 | } else { 251 | // If no start point was ever found, we assume full stretch. 252 | endY = raster.width - 2; 253 | } 254 | 255 | if (scale != 1) { 256 | startX = Std.int(startX * scale); 257 | endX = Std.int(endX * scale); 258 | startY = Std.int(startY * scale); 259 | endY = Std.int(endY * scale); 260 | } 261 | 262 | return [startX, endX, startY, endY]; 263 | } 264 | 265 | /** Returns the pads, or null if the image had no pads or the pads match the splits. Pads are an int[4] that has left, right, 266 | * top, bottom. */ 267 | private function getPads (raster:BitmapData, name:String, splits:Array):Array { 268 | var bottom:Int = raster.height - 1; 269 | var right:Int = raster.width - 1; 270 | 271 | var startX:Int = getSplitPoint(raster, name, 1, bottom, true, true); 272 | var startY:Int = getSplitPoint(raster, name, right, 1, true, false); 273 | 274 | // No need to hunt for the end if a start was never found. 275 | var endX:Int = 0; 276 | var endY:Int = 0; 277 | if (startX != 0) endX = getSplitPoint(raster, name, startX + 1, bottom, false, true); 278 | if (startY != 0) endY = getSplitPoint(raster, name, right, startY + 1, false, false); 279 | 280 | // Ensure pixels after the end are not invalid. 281 | getSplitPoint(raster, name, endX + 1, bottom, true, true); 282 | getSplitPoint(raster, name, right, endY + 1, true, false); 283 | 284 | // No pads. 285 | if (startX == 0 && endX == 0 && startY == 0 && endY == 0) { 286 | return null; 287 | } 288 | 289 | // -2 here is because the coordinates were computed before the 1px border was stripped. 290 | if (startX == 0 && endX == 0) { 291 | startX = -1; 292 | endX = -1; 293 | } else { 294 | if (startX > 0) { 295 | startX--; 296 | endX = raster.width - 2 - (endX - 1); 297 | } else { 298 | // If no start point was ever found, we assume full stretch. 299 | endX = raster.width - 2; 300 | } 301 | } 302 | if (startY == 0 && endY == 0) { 303 | startY = -1; 304 | endY = -1; 305 | } else { 306 | if (startY > 0) { 307 | startY--; 308 | endY = raster.height - 2 - (endY - 1); 309 | } else { 310 | // If no start point was ever found, we assume full stretch. 311 | endY = raster.height - 2; 312 | } 313 | } 314 | 315 | if (scale != 1) { 316 | startX = Std.int(startX * scale); 317 | endX = Std.int(endX * scale); 318 | startY = Std.int(startY * scale); 319 | endY = Std.int(endY * scale); 320 | } 321 | 322 | var pads:Array = [startX, endX, startY, endY]; 323 | 324 | if (splits != null && pads == splits) { 325 | return null; 326 | } 327 | 328 | return pads; 329 | } 330 | 331 | /** Hunts for the start or end of a sequence of split pixels. Begins searching at (startX, startY) then follows along the x or y 332 | * axis (depending on value of xAxis) for the first non-transparent pixel if startPoint is true, or the first transparent pixel 333 | * if startPoint is false. Returns 0 if none found, as 0 is considered an invalid split point being in the outer border which 334 | * will be stripped. */ 335 | static function getSplitPoint (raster:BitmapData, name:String, startX:Int, startY:Int, startPoint:Bool, xAxis:Bool):Int { 336 | var rgba:Array; 337 | 338 | var next:Int = xAxis ? startX : startY; 339 | var end:Int = xAxis ? raster.width : raster.width; 340 | var breakA:Int = startPoint ? 255 : 0; 341 | 342 | var x:Int = startX; 343 | var y:Int = startY; 344 | while (next != end) { 345 | if (xAxis) 346 | x = next; 347 | else 348 | y = next; 349 | 350 | rgba = Utils.getRGBA(raster.getPixel32(x, y)); 351 | if (rgba[3] == breakA) return next; 352 | 353 | if (!startPoint && (rgba[0] != 0 || rgba[1] != 0 || rgba[2] != 0 || rgba[3] != 255)) splitError(x, y, rgba, name); 354 | 355 | next++; 356 | } 357 | 358 | return 0; 359 | } 360 | 361 | static function hash (image:BitmapData):String { 362 | return Sha1.encode(""+image.getPixels(image.rect)); 363 | } 364 | } 365 | -------------------------------------------------------------------------------- /hxpk/TexturePacker.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | import haxe.io.Path; 4 | import haxe.io.Output; 5 | import flash.display.BitmapData; 6 | import flash.geom.Point; 7 | import flash.geom.Rectangle; 8 | import flash.geom.Matrix; 9 | import flash.utils.ByteArray; 10 | 11 | 12 | class TexturePacker { 13 | private var settings:Settings; 14 | private var packer:Packer; 15 | private var imageProcessor:ImageProcessor; 16 | private var inputImages:Array = new Array(); 17 | private var rootDir:String; 18 | 19 | /** @param rootDir Used to strip the root directory prefix from image file names, can be null. */ 20 | public function new (rootDir:String, settings:Settings) { 21 | this.rootDir = rootDir; 22 | this.settings = settings; 23 | 24 | if (settings.pot) { 25 | if (settings.maxWidth != Utils.nextPowerOfTwo(settings.maxWidth)) 26 | throw "If pot is true, maxWidth must be a power of two: " + settings.maxWidth; 27 | if (settings.maxHeight != Utils.nextPowerOfTwo(settings.maxHeight)) 28 | throw "If pot is true, maxHeight must be a power of two: " + settings.maxHeight; 29 | } 30 | 31 | if (settings.grid) 32 | packer = new GridPacker(settings); 33 | else 34 | packer = new MaxRectsPacker(settings); 35 | imageProcessor = new ImageProcessor(rootDir, settings); 36 | } 37 | 38 | public function addImageFile (file:String):Void { 39 | var inputImage:InputImage = new InputImage(); 40 | inputImage.file = file; 41 | inputImages.push(inputImage); 42 | } 43 | 44 | public function addImageBitmapData (image:BitmapData, name:String):Void { 45 | var inputImage:InputImage = new InputImage(); 46 | inputImage.image = image; 47 | inputImage.name = name; 48 | inputImages.push(inputImage); 49 | } 50 | 51 | public function pack (outputDir:String, packFileName:String):Void { 52 | Settings.environment.createDirectory(outputDir); 53 | 54 | for (i in 0 ... settings.scale.length) { 55 | imageProcessor.setScale(settings.scale[i]); 56 | for (inputImage in inputImages) { 57 | if (inputImage.file != null) 58 | imageProcessor.addImageFile(inputImage.file); 59 | else 60 | imageProcessor.addImageBitmapData(inputImage.image, inputImage.name); 61 | } 62 | 63 | var pages:Array = packer.pack(imageProcessor.getImages()); 64 | 65 | var scaledPackFileName:String = settings.scaledPackFileName(packFileName, i); 66 | var packFile:String = Path.join([outputDir, scaledPackFileName]); 67 | var packDir:String = outputDir; 68 | Settings.environment.createDirectory(packDir); 69 | 70 | writeImages(packFile, pages); 71 | try { 72 | Utils.print('writing packfile: ' + packFile); 73 | writePackFile(packFile, pages); 74 | } catch (e:Dynamic) { 75 | throw "Error writing pack file: " + e; 76 | } 77 | Utils.print('done'); 78 | imageProcessor.clear(); 79 | } 80 | } 81 | 82 | private function writeImages (packFile:String, pages:Array):Void { 83 | var packDir:String = Path.directory(packFile); 84 | var imageName:String = Path.withoutDirectory(packFile); 85 | var dotIndex:Int = imageName.indexOf('.'); 86 | if (dotIndex != -1) imageName = imageName.substr(0, dotIndex); 87 | 88 | var fileIndex:Int = 0; 89 | for (page in pages) { 90 | var width:Int = page.width, height:Int = page.height; 91 | var paddingX:Int = settings.paddingX; 92 | var paddingY:Int = settings.paddingY; 93 | if (settings.duplicatePadding) { 94 | paddingX = Std.int(paddingX / 2); 95 | paddingY = Std.int(paddingY / 2); 96 | } 97 | width -= settings.paddingX; 98 | height -= settings.paddingY; 99 | if (settings.edgePadding) { 100 | page.x = paddingX; 101 | page.y = paddingY; 102 | width += paddingX * 2; 103 | height += paddingY * 2; 104 | } 105 | if (settings.pot) { 106 | width = Utils.nextPowerOfTwo(width); 107 | height = Utils.nextPowerOfTwo(height); 108 | } 109 | width = Std.int(Math.max(settings.minWidth, width)); 110 | height = Std.int(Math.max(settings.minHeight, height)); 111 | 112 | var outputFile:String = ""; 113 | while (true) { 114 | outputFile = Path.join([packDir, imageName + (fileIndex++ == 0 ? "" : ("" + fileIndex)) + "." + settings.outputFormat]); 115 | if (!Settings.environment.exists(outputFile)) break; 116 | } 117 | Settings.environment.createDirectory(packDir); 118 | page.imageName = Path.withoutDirectory(outputFile); 119 | 120 | var canvas:BitmapData = new BitmapData(width, height, true, 0); 121 | 122 | Utils.print("Writing " + width + "x" + height + ": " + outputFile); 123 | 124 | for (rect in page.outputRects) { 125 | var image:BitmapData = rect.getImage(imageProcessor); 126 | var iw:Int = image.width; 127 | var ih:Int = image.height; 128 | var rectX:Int = page.x + rect.x, rectY:Int = page.y + page.height - rect.y - rect.height; 129 | if (settings.duplicatePadding) { 130 | var amountX:Int = Std.int(settings.paddingX / 2); 131 | var amountY:Int = Std.int(settings.paddingY / 2); 132 | if (rect.rotated) { 133 | // Copy corner pixels to fill corners of the padding. 134 | for (i in 1 ... amountX+1) { 135 | for (j in 1 ... amountY+1) { 136 | plot(canvas, rectX - j, rectY + iw - 1 + i, image.getPixel32(0, 0)); 137 | plot(canvas, rectX + ih - 1 + j, rectY + iw - 1 + i, image.getPixel32(0, ih - 1)); 138 | plot(canvas, rectX - j, rectY - i, image.getPixel32(iw - 1, 0)); 139 | plot(canvas, rectX + ih - 1 + j, rectY - i, image.getPixel32(iw - 1, ih - 1)); 140 | } 141 | } 142 | // Copy edge pixels into padding. 143 | for (i in 1 ... amountY+1) { 144 | for (j in 0 ... iw) { 145 | plot(canvas, rectX - i, rectY + iw - 1 - j, image.getPixel32(j, 0)); 146 | plot(canvas, rectX + ih - 1 + i, rectY + iw - 1 - j, image.getPixel32(j, ih - 1)); 147 | } 148 | } 149 | for (i in 1 ... amountX+1) { 150 | for (j in 0 ... ih) { 151 | plot(canvas, rectX + j, rectY - i, image.getPixel32(iw - 1, j)); 152 | plot(canvas, rectX + j, rectY + iw - 1 + i, image.getPixel32(0, j)); 153 | } 154 | } 155 | } else { 156 | // Copy corner pixels to fill corners of the padding. 157 | for (i in 1 ... amountX+1) { 158 | for (j in 1 ... amountY+1) { 159 | plot(canvas, rectX - i, rectY - j, image.getPixel32(0, 0)); 160 | plot(canvas, rectX - i, rectY + ih - 1 + j, image.getPixel32(0, ih - 1)); 161 | plot(canvas, rectX + iw - 1 + i, rectY - j, image.getPixel32(iw - 1, 0)); 162 | plot(canvas, rectX + iw - 1 + i, rectY + ih - 1 + j, image.getPixel32(iw - 1, ih - 1)); 163 | } 164 | } 165 | // Copy edge pixels into padding. 166 | for (i in 1 ... amountY+1) { 167 | copy(image, 0, 0, iw, 1, canvas, rectX, rectY - i, rect.rotated); 168 | copy(image, 0, ih - 1, iw, 1, canvas, rectX, rectY + ih - 1 + i, rect.rotated); 169 | } 170 | for (i in 1 ... amountX+1) { 171 | copy(image, 0, 0, 1, ih, canvas, rectX - i, rectY, rect.rotated); 172 | copy(image, iw - 1, 0, 1, ih, canvas, rectX + iw - 1 + i, rectY, rect.rotated); 173 | } 174 | } 175 | } 176 | copy(image, 0, 0, iw, ih, canvas, rectX, rectY, rect.rotated); 177 | } 178 | 179 | if (settings.bleed && !settings.premultiplyAlpha && !(settings.outputFormat.toLowerCase() == "jpg")) { 180 | canvas = new ColorBleedEffect().processImage(canvas, 2); 181 | } 182 | 183 | var error:String = null; 184 | try { 185 | Settings.environment.saveImage(canvas, outputFile, settings); 186 | } catch (e:Dynamic) { 187 | error = "Error writing file " + outputFile + ": " + e; 188 | } 189 | 190 | if (error != null) throw error; 191 | } 192 | } 193 | 194 | static inline function plot (dst:BitmapData, x:Int, y:Int, argb:Int):Void { 195 | if (0 <= x && x < dst.width && 0 <= y && y < dst.height) dst.setPixel32(x, y, argb); 196 | } 197 | 198 | static inline function copy (src:BitmapData, x:Int, y:Int, w:Int, h:Int, dst:BitmapData, dx:Int, dy:Int, rotated:Bool):Void { 199 | if (rotated) { 200 | if (x != 0 || y != 0 || w != src.width || h != src.height) { 201 | var bmd = new BitmapData(w, h, true, 0); 202 | bmd.copyPixels(src, new Rectangle(x, y, w, h), new Point()); 203 | src = bmd; 204 | } 205 | var m = new Matrix(0, -1, 1, 0, dx, dy+w); 206 | dst.draw(src, m); 207 | } else { 208 | dst.copyPixels(src, new Rectangle(x, y, w, h), new Point(dx, dy)); 209 | } 210 | } 211 | 212 | private function writePackFile (packFile:String, pages:Array):Void { 213 | if (Settings.environment.exists(packFile)) { 214 | // Make sure there aren't duplicate names. 215 | // TODO 216 | /*TextureAtlasData textureAtlasData = new TextureAtlasData(new FileHandle(packFile), new FileHandle(packFile), false); 217 | for (Page page : pages) { 218 | for (Rect rect : page.outputRects) { 219 | String rectName = Rect.getAtlasName(rect.name, settings.flattenPaths); 220 | for (Region region : textureAtlasData.getRegions()) { 221 | if (region.name.equals(rectName)) { 222 | throw new GdxRuntimeException("A region with the name \"" + rectName + "\" has already been packed: " 223 | + rect.name); 224 | } 225 | } 226 | } 227 | }*/ 228 | } 229 | 230 | var writer:Output = Settings.environment.append(packFile); 231 | for (page in pages) { 232 | writer.writeString("\n" + page.imageName + "\n"); 233 | writer.writeString("size: " + page.width + "," + page.height + "\n"); 234 | writer.writeString("format: " + settings.format + "\n"); 235 | writer.writeString("filter: " + settings.filterMin + "," + settings.filterMag + "\n"); 236 | writer.writeString("repeat: " + getRepeatValue() + "\n"); 237 | 238 | for (rect in page.outputRects) { 239 | writeRect(writer, page, rect, rect.name); 240 | for (alias in rect.aliases.keys()) { 241 | var aliasRect:Rect = Rect.clone(rect); 242 | alias.apply(aliasRect); 243 | writeRect(writer, page, aliasRect, alias.name); 244 | } 245 | } 246 | } 247 | writer.close(); 248 | } 249 | 250 | private function writeRect (writer:Output, page:Page, rect:Rect, name:String):Void { 251 | writer.writeString(Rect.getAtlasName(name, settings.flattenPaths) + "\n"); 252 | writer.writeString(" rotate: " + rect.rotated + "\n"); 253 | writer.writeString(" xy: " + (page.x + rect.x) + ", " + (page.y + page.height - rect.height - rect.y) + "\n"); 254 | 255 | writer.writeString(" size: " + rect.regionWidth + ", " + rect.regionHeight + "\n"); 256 | if (rect.splits != null) { 257 | writer.writeString(" split: " // 258 | + rect.splits[0] + ", " + rect.splits[1] + ", " + rect.splits[2] + ", " + rect.splits[3] + "\n"); 259 | } 260 | if (rect.pads != null) { 261 | if (rect.splits == null) writer.writeString(" split: 0, 0, 0, 0\n"); 262 | writer.writeString(" pad: " + rect.pads[0] + ", " + rect.pads[1] + ", " + rect.pads[2] + ", " + rect.pads[3] + "\n"); 263 | } 264 | writer.writeString(" orig: " + rect.originalWidth + ", " + rect.originalHeight + "\n"); 265 | writer.writeString(" offset: " + rect.offsetX + ", " + (rect.originalHeight - rect.regionHeight - rect.offsetY) + "\n"); 266 | writer.writeString(" index: " + rect.index + "\n"); 267 | } 268 | 269 | private function getRepeatValue ():String { 270 | if (settings.wrapX == TextureWrap.Repeat && settings.wrapY == TextureWrap.Repeat) return "xy"; 271 | if (settings.wrapX == TextureWrap.Repeat && settings.wrapY == TextureWrap.ClampToEdge) return "x"; 272 | if (settings.wrapX == TextureWrap.ClampToEdge && settings.wrapY == TextureWrap.Repeat) return "y"; 273 | return "none"; 274 | } 275 | 276 | /** @param input Directory containing individual images to be packed. 277 | * @param output Directory where the pack file and page images will be written. 278 | * @param packFileName The name of the pack file. Also used to name the page images. */ 279 | static public function process (input:String, output:String, packFileName:String, ?settings:Settings=null):Void { 280 | // default settings 281 | if (settings == null) settings = new Settings(); 282 | 283 | //try { 284 | var processor:TexturePackerFileProcessor = new TexturePackerFileProcessor(settings, packFileName); 285 | // Sort input files by name to avoid platform-dependent atlas output changes. 286 | processor.setComparator(function (file1:String, file2:String) { 287 | return Utils.stringCompare(file1, file2); 288 | }); 289 | processor.process(input, output); 290 | //} catch (e:Dynamic) { 291 | // throw "Error packing files: " + e; 292 | //} 293 | } 294 | 295 | /** @return true if the output file does not yet exist or its last modification date is before the last modification date of the 296 | * input file */ 297 | static public function isModified (input:String, output:String, packFileName:String):Bool { 298 | var outputFile:String = output; 299 | outputFile = Path.join([outputFile, packFileName]); 300 | if (!Settings.environment.exists(outputFile)) return true; 301 | 302 | var inputFile:String = input; 303 | if (!Settings.environment.exists(inputFile)) throw "Input file does not exist: " + inputFile; 304 | return Settings.environment.newerThan(inputFile, outputFile); 305 | } 306 | 307 | static public function processIfModified (input:String, output:String, packFileName:String, ?settings:Settings=null):Void { 308 | if (isModified(input, output, packFileName)) process(input, output, packFileName, settings); 309 | } 310 | 311 | static public function main ():Void { 312 | Settings.environment = new hxpk.environment.FileSystem(); 313 | 314 | var args:Array = Sys.args(); 315 | var cwd = args.pop(); 316 | Settings.environment.setCwd(cwd); 317 | var input:String = null, output:String = null, packFileName:String = "pack.atlas"; 318 | 319 | if (args.length > 0) { 320 | input = args[0]; 321 | if (args.length > 1) output = args[1]; 322 | if (args.length > 2) packFileName = args[2]; 323 | } else { 324 | Utils.print("Usage: inputDir [outputDir] [packFileName]"); 325 | return; 326 | } 327 | 328 | if (output == null) { 329 | var inputDir = Path.removeTrailingSlashes(Settings.environment.fullPath(input)); 330 | output = inputDir + "-packed"; 331 | Settings.environment.createDirectory(output); 332 | Utils.print(output); 333 | } 334 | 335 | process(input, output, packFileName); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /hxpk/MaxRects.hx: -------------------------------------------------------------------------------- 1 | package hxpk; 2 | 3 | 4 | /** Maximal rectangles bin packing algorithm. Adapted from this C++ public domain source: 5 | * http://clb.demon.fi/projects/even-more-rectangle-bin-packing */ 6 | @:allow(hxpk.MaxRectsPacker) 7 | class MaxRects { 8 | var settings:Settings; 9 | 10 | var binWidth:Int = 0; 11 | var binHeight:Int = 0; 12 | var usedRectangles:Array = new Array(); 13 | var freeRectangles:Array = new Array(); 14 | 15 | public function new (?settings:Settings=null) { 16 | this.settings = settings; 17 | } 18 | 19 | public function init (width:Int, height:Int):Void { 20 | binWidth = width; 21 | binHeight = height; 22 | 23 | usedRectangles.splice(0, usedRectangles.length); 24 | freeRectangles.splice(0, freeRectangles.length); 25 | var n:Rect = new Rect(); 26 | n.x = 0; 27 | n.y = 0; 28 | n.width = width; 29 | n.height = height; 30 | freeRectangles.push(n); 31 | } 32 | 33 | /** Packs a single image. Order is defined externally. */ 34 | public function insert (rect:Rect, method:FreeRectChoiceHeuristic):Rect { 35 | var newNode:Rect = scoreRect(rect, method); 36 | if (newNode.height == 0) return null; 37 | 38 | var numRectanglesToProcess:Int = freeRectangles.length; 39 | var i:Int = 0; 40 | while (i < numRectanglesToProcess) { 41 | if (splitFreeNode(freeRectangles[i], newNode)) { 42 | Utils.removeIndex(freeRectangles, i); 43 | --i; 44 | --numRectanglesToProcess; 45 | } 46 | ++i; 47 | } 48 | 49 | pruneFreeList(); 50 | 51 | var bestNode:Rect = new Rect(); 52 | bestNode.set(rect); 53 | bestNode.score1 = newNode.score1; 54 | bestNode.score2 = newNode.score2; 55 | bestNode.x = newNode.x; 56 | bestNode.y = newNode.y; 57 | bestNode.width = newNode.width; 58 | bestNode.height = newNode.height; 59 | bestNode.rotated = newNode.rotated; 60 | 61 | usedRectangles.push(bestNode); 62 | return bestNode; 63 | } 64 | 65 | /** For each rectangle, packs each one then chooses the best and packs that. Slow! */ 66 | public function pack (rects:Array, method:FreeRectChoiceHeuristic):Page { 67 | rects = rects.copy(); 68 | while (rects.length > 0) { 69 | var bestRectIndex:Int = -1; 70 | var bestNode:Rect = new Rect(); 71 | bestNode.score1 = Utils.MAX_INT; 72 | bestNode.score2 = Utils.MAX_INT; 73 | 74 | // Find the next rectangle that packs best. 75 | for (i in 0 ... rects.length) { 76 | var newNode:Rect = scoreRect(rects[i], method); 77 | if (newNode.score1 < bestNode.score1 || (newNode.score1 == bestNode.score1 && newNode.score2 < bestNode.score2)) { 78 | bestNode.set(rects[i]); 79 | bestNode.score1 = newNode.score1; 80 | bestNode.score2 = newNode.score2; 81 | bestNode.x = newNode.x; 82 | bestNode.y = newNode.y; 83 | bestNode.width = newNode.width; 84 | bestNode.height = newNode.height; 85 | bestNode.rotated = newNode.rotated; 86 | bestRectIndex = i; 87 | } 88 | } 89 | 90 | if (bestRectIndex == -1) break; 91 | 92 | placeRect(bestNode); 93 | Utils.removeIndex(rects, bestRectIndex); 94 | } 95 | 96 | var result:Page = getResult(); 97 | result.remainingRects = rects; 98 | return result; 99 | } 100 | 101 | public function getResult ():Page { 102 | var w:Int = 0, h:Int = 0; 103 | for (i in 0 ... usedRectangles.length) { 104 | var rect:Rect = usedRectangles[i]; 105 | w = Std.int(Math.max(w, rect.x + rect.width)); 106 | h = Std.int(Math.max(h, rect.y + rect.height)); 107 | } 108 | var result:Page = new Page(); 109 | result.outputRects = usedRectangles.copy(); 110 | result.occupancy = getOccupancy(); 111 | result.width = w; 112 | result.height = h; 113 | return result; 114 | } 115 | 116 | private function placeRect (node:Rect):Void { 117 | var numRectanglesToProcess:Int = freeRectangles.length; 118 | var i:Int = 0; 119 | while (i < numRectanglesToProcess) { 120 | if (splitFreeNode(freeRectangles[i], node)) { 121 | Utils.removeIndex(freeRectangles, i); 122 | --i; 123 | --numRectanglesToProcess; 124 | } 125 | i++; 126 | } 127 | 128 | pruneFreeList(); 129 | 130 | usedRectangles.push(node); 131 | } 132 | 133 | private function scoreRect (rect:Rect, method:FreeRectChoiceHeuristic):Rect { 134 | var width:Int = rect.width; 135 | var height:Int = rect.height; 136 | var rotatedWidth:Int = height - settings.paddingY + settings.paddingX; 137 | var rotatedHeight:Int = width - settings.paddingX + settings.paddingY; 138 | var rotate:Bool = rect.canRotate && settings.rotation; 139 | 140 | var newNode:Rect = null; 141 | switch (method) { 142 | case BestShortSideFit: 143 | newNode = findPositionForNewNodeBestShortSideFit(width, height, rotatedWidth, rotatedHeight, rotate); 144 | case BottomLeftRule: 145 | newNode = findPositionForNewNodeBottomLeft(width, height, rotatedWidth, rotatedHeight, rotate); 146 | case ContactPointRule: 147 | newNode = findPositionForNewNodeContactPoint(width, height, rotatedWidth, rotatedHeight, rotate); 148 | newNode.score1 = -newNode.score1; // Reverse since we are minimizing, but for contact point score bigger is better. 149 | case BestLongSideFit: 150 | newNode = findPositionForNewNodeBestLongSideFit(width, height, rotatedWidth, rotatedHeight, rotate); 151 | case BestAreaFit: 152 | newNode = findPositionForNewNodeBestAreaFit(width, height, rotatedWidth, rotatedHeight, rotate); 153 | } 154 | 155 | // Cannot fit the current rectangle. 156 | if (newNode.height == 0) { 157 | newNode.score1 = Utils.MAX_INT; 158 | newNode.score2 = Utils.MAX_INT; 159 | } 160 | 161 | return newNode; 162 | } 163 | 164 | // / Computes the ratio of used surface area. 165 | private function getOccupancy ():Float { 166 | var usedSurfaceArea:Int = 0; 167 | for (i in 0 ... usedRectangles.length) 168 | usedSurfaceArea += usedRectangles[i].width * usedRectangles[i].height; 169 | return usedSurfaceArea / (binWidth * binHeight); 170 | } 171 | 172 | private function findPositionForNewNodeBottomLeft (width:Int, height:Int, rotatedWidth:Int, rotatedHeight:Int, rotate:Bool):Rect { 173 | var bestNode:Rect = new Rect(); 174 | 175 | bestNode.score1 = Utils.MAX_INT; // best y, score2 is best x 176 | 177 | for (i in 0 ... freeRectangles.length) { 178 | // Try to place the rectangle in upright (non-rotated) orientation. 179 | if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { 180 | var topSideY:Int = freeRectangles[i].y + height; 181 | if (topSideY < bestNode.score1 || (topSideY == bestNode.score1 && freeRectangles[i].x < bestNode.score2)) { 182 | bestNode.x = freeRectangles[i].x; 183 | bestNode.y = freeRectangles[i].y; 184 | bestNode.width = width; 185 | bestNode.height = height; 186 | bestNode.score1 = topSideY; 187 | bestNode.score2 = freeRectangles[i].x; 188 | bestNode.rotated = false; 189 | } 190 | } 191 | if (rotate && freeRectangles[i].width >= rotatedWidth && freeRectangles[i].height >= rotatedHeight) { 192 | var topSideY:Int = freeRectangles[i].y + rotatedHeight; 193 | if (topSideY < bestNode.score1 || (topSideY == bestNode.score1 && freeRectangles[i].x < bestNode.score2)) { 194 | bestNode.x = freeRectangles[i].x; 195 | bestNode.y = freeRectangles[i].y; 196 | bestNode.width = rotatedWidth; 197 | bestNode.height = rotatedHeight; 198 | bestNode.score1 = topSideY; 199 | bestNode.score2 = freeRectangles[i].x; 200 | bestNode.rotated = true; 201 | } 202 | } 203 | } 204 | return bestNode; 205 | } 206 | 207 | private function findPositionForNewNodeBestShortSideFit (width:Int, height:Int, rotatedWidth:Int, rotatedHeight:Int, rotate:Bool):Rect { 208 | var bestNode:Rect = new Rect(); 209 | bestNode.score1 = Utils.MAX_INT; 210 | 211 | for (i in 0 ... freeRectangles.length) { 212 | // Try to place the rectangle in upright (non-rotated) orientation. 213 | if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { 214 | var leftoverHoriz:Int = Std.int(Math.abs(freeRectangles[i].width - width)); 215 | var leftoverVert:Int = Std.int(Math.abs(freeRectangles[i].height - height)); 216 | var shortSideFit:Int = Std.int(Math.min(leftoverHoriz, leftoverVert)); 217 | var longSideFit:Int = Std.int(Math.max(leftoverHoriz, leftoverVert)); 218 | 219 | if (shortSideFit < bestNode.score1 || (shortSideFit == bestNode.score1 && longSideFit < bestNode.score2)) { 220 | bestNode.x = freeRectangles[i].x; 221 | bestNode.y = freeRectangles[i].y; 222 | bestNode.width = width; 223 | bestNode.height = height; 224 | bestNode.score1 = shortSideFit; 225 | bestNode.score2 = longSideFit; 226 | bestNode.rotated = false; 227 | } 228 | } 229 | 230 | if (rotate && freeRectangles[i].width >= rotatedWidth && freeRectangles[i].height >= rotatedHeight) { 231 | var flippedLeftoverHoriz:Int = Std.int(Math.abs(freeRectangles[i].width - rotatedWidth)); 232 | var flippedLeftoverVert:Int = Std.int(Math.abs(freeRectangles[i].height - rotatedHeight)); 233 | var flippedShortSideFit:Int = Std.int(Math.min(flippedLeftoverHoriz, flippedLeftoverVert)); 234 | var flippedLongSideFit:Int = Std.int(Math.max(flippedLeftoverHoriz, flippedLeftoverVert)); 235 | 236 | if (flippedShortSideFit < bestNode.score1 237 | || (flippedShortSideFit == bestNode.score1 && flippedLongSideFit < bestNode.score2)) { 238 | bestNode.x = freeRectangles[i].x; 239 | bestNode.y = freeRectangles[i].y; 240 | bestNode.width = rotatedWidth; 241 | bestNode.height = rotatedHeight; 242 | bestNode.score1 = flippedShortSideFit; 243 | bestNode.score2 = flippedLongSideFit; 244 | bestNode.rotated = true; 245 | } 246 | } 247 | } 248 | 249 | return bestNode; 250 | } 251 | 252 | private function findPositionForNewNodeBestLongSideFit (width:Int, height:Int, rotatedWidth:Int, rotatedHeight:Int, rotate:Bool):Rect { 253 | var bestNode:Rect = new Rect(); 254 | 255 | bestNode.score2 = Utils.MAX_INT; 256 | 257 | for (i in 0 ... freeRectangles.length) { 258 | // Try to place the rectangle in upright (non-rotated) orientation. 259 | if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { 260 | var leftoverHoriz:Int = Std.int(Math.abs(freeRectangles[i].width - width)); 261 | var leftoverVert:Int = Std.int(Math.abs(freeRectangles[i].height - height)); 262 | var shortSideFit:Int = Std.int(Math.min(leftoverHoriz, leftoverVert)); 263 | var longSideFit:Int = Std.int(Math.max(leftoverHoriz, leftoverVert)); 264 | 265 | if (longSideFit < bestNode.score2 || (longSideFit == bestNode.score2 && shortSideFit < bestNode.score1)) { 266 | bestNode.x = freeRectangles[i].x; 267 | bestNode.y = freeRectangles[i].y; 268 | bestNode.width = width; 269 | bestNode.height = height; 270 | bestNode.score1 = shortSideFit; 271 | bestNode.score2 = longSideFit; 272 | bestNode.rotated = false; 273 | } 274 | } 275 | 276 | if (rotate && freeRectangles[i].width >= rotatedWidth && freeRectangles[i].height >= rotatedHeight) { 277 | var leftoverHoriz:Int = Std.int(Math.abs(freeRectangles[i].width - rotatedWidth)); 278 | var leftoverVert:Int = Std.int(Math.abs(freeRectangles[i].height - rotatedHeight)); 279 | var shortSideFit:Int = Std.int(Math.min(leftoverHoriz, leftoverVert)); 280 | var longSideFit:Int = Std.int(Math.max(leftoverHoriz, leftoverVert)); 281 | 282 | if (longSideFit < bestNode.score2 || (longSideFit == bestNode.score2 && shortSideFit < bestNode.score1)) { 283 | bestNode.x = freeRectangles[i].x; 284 | bestNode.y = freeRectangles[i].y; 285 | bestNode.width = rotatedWidth; 286 | bestNode.height = rotatedHeight; 287 | bestNode.score1 = shortSideFit; 288 | bestNode.score2 = longSideFit; 289 | bestNode.rotated = true; 290 | } 291 | } 292 | } 293 | return bestNode; 294 | } 295 | 296 | private function findPositionForNewNodeBestAreaFit (width:Int, height:Int, rotatedWidth:Int, rotatedHeight:Int, rotate:Bool):Rect { 297 | var bestNode:Rect = new Rect(); 298 | 299 | bestNode.score1 = Utils.MAX_INT; // best area fit, score2 is best short side fit 300 | 301 | for (i in 0 ... freeRectangles.length) { 302 | var areaFit:Int = freeRectangles[i].width * freeRectangles[i].height - width * height; 303 | 304 | // Try to place the rectangle in upright (non-rotated) orientation. 305 | if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { 306 | var leftoverHoriz:Int = Std.int(Math.abs(freeRectangles[i].width - width)); 307 | var leftoverVert:Int = Std.int(Math.abs(freeRectangles[i].height - height)); 308 | var shortSideFit:Int = Std.int(Math.min(leftoverHoriz, leftoverVert)); 309 | 310 | if (areaFit < bestNode.score1 || (areaFit == bestNode.score1 && shortSideFit < bestNode.score2)) { 311 | bestNode.x = freeRectangles[i].x; 312 | bestNode.y = freeRectangles[i].y; 313 | bestNode.width = width; 314 | bestNode.height = height; 315 | bestNode.score2 = shortSideFit; 316 | bestNode.score1 = areaFit; 317 | bestNode.rotated = false; 318 | } 319 | } 320 | 321 | if (rotate && freeRectangles[i].width >= rotatedWidth && freeRectangles[i].height >= rotatedHeight) { 322 | var leftoverHoriz:Int = Std.int(Math.abs(freeRectangles[i].width - rotatedWidth)); 323 | var leftoverVert:Int = Std.int(Math.abs(freeRectangles[i].height - rotatedHeight)); 324 | var shortSideFit:Int = Std.int(Math.min(leftoverHoriz, leftoverVert)); 325 | 326 | if (areaFit < bestNode.score1 || (areaFit == bestNode.score1 && shortSideFit < bestNode.score2)) { 327 | bestNode.x = freeRectangles[i].x; 328 | bestNode.y = freeRectangles[i].y; 329 | bestNode.width = rotatedWidth; 330 | bestNode.height = rotatedHeight; 331 | bestNode.score2 = shortSideFit; 332 | bestNode.score1 = areaFit; 333 | bestNode.rotated = true; 334 | } 335 | } 336 | } 337 | return bestNode; 338 | } 339 | 340 | // / Returns 0 if the two intervals i1 and i2 are disjoint, or the length of their overlap otherwise. 341 | private function commonIntervalLength (i1start:Int, i1end:Int, i2start:Int, i2end:Int):Int { 342 | if (i1end < i2start || i2end < i1start) return 0; 343 | return Std.int(Math.min(i1end, i2end) - Math.max(i1start, i2start)); 344 | } 345 | 346 | private function contactPointScoreNode (x:Int, y:Int, width:Int, height:Int):Int { 347 | var score:Int = 0; 348 | 349 | if (x == 0 || x + width == binWidth) score += height; 350 | if (y == 0 || y + height == binHeight) score += width; 351 | 352 | for (i in 0 ... usedRectangles.length) { 353 | if (usedRectangles[i].x == x + width || usedRectangles[i].x + usedRectangles[i].width == x) 354 | score += commonIntervalLength(usedRectangles[i].y, usedRectangles[i].y + usedRectangles[i].height, y, 355 | y + height); 356 | if (usedRectangles[i].y == y + height || usedRectangles[i].y + usedRectangles[i].height == y) 357 | score += commonIntervalLength(usedRectangles[i].x, usedRectangles[i].x + usedRectangles[i].width, x, x 358 | + width); 359 | } 360 | return score; 361 | } 362 | 363 | private function findPositionForNewNodeContactPoint (width:Int, height:Int, rotatedWidth:Int, rotatedHeight:Int, rotate:Bool):Rect { 364 | var bestNode:Rect = new Rect(); 365 | 366 | bestNode.score1 = -1; // best contact score 367 | 368 | for (i in 0 ... freeRectangles.length) { 369 | // Try to place the rectangle in upright (non-rotated) orientation. 370 | if (freeRectangles[i].width >= width && freeRectangles[i].height >= height) { 371 | var score:Int = contactPointScoreNode(freeRectangles[i].x, freeRectangles[i].y, width, height); 372 | if (score > bestNode.score1) { 373 | bestNode.x = freeRectangles[i].x; 374 | bestNode.y = freeRectangles[i].y; 375 | bestNode.width = width; 376 | bestNode.height = height; 377 | bestNode.score1 = score; 378 | bestNode.rotated = false; 379 | } 380 | } 381 | if (rotate && freeRectangles[i].width >= rotatedWidth && freeRectangles[i].height >= rotatedHeight) { 382 | // This was width,height -- bug fixed? 383 | var score:Int = contactPointScoreNode(freeRectangles[i].x, freeRectangles[i].y, rotatedWidth, rotatedHeight); 384 | if (score > bestNode.score1) { 385 | bestNode.x = freeRectangles[i].x; 386 | bestNode.y = freeRectangles[i].y; 387 | bestNode.width = rotatedWidth; 388 | bestNode.height = rotatedHeight; 389 | bestNode.score1 = score; 390 | bestNode.rotated = true; 391 | } 392 | } 393 | } 394 | return bestNode; 395 | } 396 | 397 | private function splitFreeNode (freeNode:Rect, usedNode:Rect):Bool { 398 | // Test with SAT if the rectangles even intersect. 399 | if (usedNode.x >= freeNode.x + freeNode.width || usedNode.x + usedNode.width <= freeNode.x 400 | || usedNode.y >= freeNode.y + freeNode.height || usedNode.y + usedNode.height <= freeNode.y) return false; 401 | 402 | if (usedNode.x < freeNode.x + freeNode.width && usedNode.x + usedNode.width > freeNode.x) { 403 | // New node at the top side of the used node. 404 | if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.height) { 405 | var newNode:Rect = Rect.clone(freeNode); 406 | newNode.height = usedNode.y - newNode.y; 407 | freeRectangles.push(newNode); 408 | } 409 | 410 | // New node at the bottom side of the used node. 411 | if (usedNode.y + usedNode.height < freeNode.y + freeNode.height) { 412 | var newNode:Rect = Rect.clone(freeNode); 413 | newNode.y = usedNode.y + usedNode.height; 414 | newNode.height = freeNode.y + freeNode.height - (usedNode.y + usedNode.height); 415 | freeRectangles.push(newNode); 416 | } 417 | } 418 | 419 | if (usedNode.y < freeNode.y + freeNode.height && usedNode.y + usedNode.height > freeNode.y) { 420 | // New node at the left side of the used node. 421 | if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.width) { 422 | var newNode:Rect = Rect.clone(freeNode); 423 | newNode.width = usedNode.x - newNode.x; 424 | freeRectangles.push(newNode); 425 | } 426 | 427 | // New node at the right side of the used node. 428 | if (usedNode.x + usedNode.width < freeNode.x + freeNode.width) { 429 | var newNode:Rect = Rect.clone(freeNode); 430 | newNode.x = usedNode.x + usedNode.width; 431 | newNode.width = freeNode.x + freeNode.width - (usedNode.x + usedNode.width); 432 | freeRectangles.push(newNode); 433 | } 434 | } 435 | 436 | return true; 437 | } 438 | 439 | private function pruneFreeList ():Void { 440 | /* 441 | * /// Would be nice to do something like this, to avoid a Theta(n^2) loop through each pair. /// But unfortunately it 442 | * doesn't quite cut it, since we also want to detect containment. /// Perhaps there's another way to do this faster than 443 | * Theta(n^2). 444 | * 445 | * if (freeRectangles.length > 0) clb::sort::QuickSort(&freeRectangles[0], freeRectangles.length, NodeSortCmp); 446 | * 447 | * for(int i = 0; i < freeRectangles.length-1; i++) if (freeRectangles[i].x == freeRectangles[i+1].x && freeRectangles[i].y 448 | * == freeRectangles[i+1].y && freeRectangles[i].width == freeRectangles[i+1].width && freeRectangles[i].height == 449 | * freeRectangles[i+1].height) { freeRectangles.erase(freeRectangles.begin() + i); --i; } 450 | */ 451 | 452 | // / Go through each pair and remove any rectangle that is redundant. 453 | var i:Int = 0; 454 | while (i < freeRectangles.length) { 455 | var j:Int = i + 1; 456 | while (j < freeRectangles.length) { 457 | if (isContainedIn(freeRectangles[i], freeRectangles[j])) { 458 | Utils.removeIndex(freeRectangles, i); 459 | --i; 460 | break; 461 | } 462 | if (isContainedIn(freeRectangles[j], freeRectangles[i])) { 463 | Utils.removeIndex(freeRectangles, j); 464 | --j; 465 | } 466 | ++j; 467 | } 468 | ++i; 469 | } 470 | } 471 | 472 | private function isContainedIn (a:Rect, b:Rect):Bool { 473 | return a.x >= b.x && a.y >= b.y && a.x + a.width <= b.x + b.width && a.y + a.height <= b.y + b.height; 474 | } 475 | } 476 | --------------------------------------------------------------------------------