diff --git a/FramePacker.as b/FramePacker.as index 6508f32..8f6f97b 100755 --- a/FramePacker.as +++ b/FramePacker.as @@ -1,91 +1,124 @@ package { + import flash.geom.*; + import flash.display.*; public class FramePacker { + private var sheets:Array; // [RectanglePacker] private var frames:Array; // [FrameInfo] - public void Pack(frames:Array) + public void FramePacker(frames:Array) { this.frames = frames.filter(function(i:FrameInfo) { return i.frame != null; }); } +import Packer.RectanglePacker; +import Packer.RectangleNode; - public function DetermineMinimumSheetSizeForClips(frames:Array):Point + private function GetMaxSize(frames:Array):Point { - - /* TODO - FOR LARGE SIZED CHUNKS BETTER HANDLING FOR FINDING MINIMUM SIZE! */ - - var areas:Array = new Array(); - - for (var i = 0; i < frames.length; i++) + if (frames.length == 0) return new Point(); + return new Point(Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.width; })), + Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.width; }))); + } + + private function GetArea(frames:Array):int + { + var area = 0; + for (var i in frames) { - are + if (frames[i] instanceof FrameInfo) + { + area += frames[i].frame.width * frames[i].frame.height; + } + if (frames[i] instanceof BitmapData || frames[i] instanceof Rectangle) + { + area += frames[i].width * frames[i].height; + } } - var areas:Array = frames.map(function(i:FrameInfo) { return i.frame.width * i.frame.height; }); - var maxW = Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.width; })); - var maxH = Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.height; })); - var max = Math.max(maxW, maxH); - - var sorted:Array = areas.sort(Array.RETURNINDEXEDARRAY | Array.NUMERIC | Array.DESCENDING); - - var exponent = Math.ceil(Math.log(max) / Math.log(2)); - max = Math.pow(2, exponent); - - //rough heuristic to see if this process is worth doing. - //it can be slow if there are a large number of big clips. - if (frames.length > 100 && max >= 512) + return area; + } + + public const AreaUsedMultiplier = 1.3; // guess how much more sheet space we use than actual space taken up by frames + + public function Pack():Boolean + { + sheets = new Array(); + var index = 0; + while (index < frames.length) { - var s = Math.max(max, 1024); - return new Point(s, s); - } + var now:Array = frames.slice(index); - sheets.push(new SpriteSheet(2048, 2048)); - for (var i = 0; i < frames.length; i++) - { - var sprite:TextureSprite = new TextureSprite(clips[sorted[i]], CACHESEQUENCES); - CacheClip(clips[sorted[i]], sprite); - sprites.push(sprite); - } - - var allUsedRects:Array = new Array(); - var nodesToVisit:Array = new Array(); - for (var i = 0; i < sheets.length; i++) - { - var sheet:SpriteSheet = sheets[i]; - var nodes = sheet.GetAllocator().GetLeafNodes(); - for (var n = 0; n < nodes.length; n++) + var max:Point = GetMaxSize(now); + var area:int = GetArea(now); + + var exponent = Math.floor(Math.log(area * AreaUsedMultiplier) / Math.log(2)); + var sizeX:int = Math.pow(2, exponent); + var sizeY:int = startSizeX; + + var sheet:RectanglePacker = new RectanglePacker(sizeX, sizeY); + + var startIndex = index; + while (index < frames.length) { - allUsedRects.push(nodes[n].Rect); - } - } - - var sizeFound = false; - var sheetSizeX = 64; - var sheetSizeY = 64; - while(!sizeFound) - { - var sheet:SpriteSheet = new SpriteSheet(sheetSizeX, sheetSizeY); - sizeFound = true; - for(var i = 0; i < allUsedRects.length; i++) - { - var node:RectangleNode = sheet.GetAllocator().Insert(allUsedRects[i].width, allUsedRects[i].height); - if (node == null) + var done = false; + var nodes:Array = new Array(); + var sorted:Array = now.sortOn(now, Array.RETURNINDEXEDARRAY | Array.DESCENDING, function(a:FrameInfo, b:FrameInfo) { return (a.frame.width * a.frame.height) - (b.frame.width - b.frame.height); }); + for (var sortedI = 0; sortedI < now.length; sortedI++) { - sizeFound = false; - break; + var i = nodes[sorted[sortedI]]; + var node:RectangleNode = sheet.Insert(now[i].frame.width, now[i].frame.height); + if (node == null) + { + if (sortedI == 0) + { + Exporter.Instance.Print("Failure: item too large to pack on sheet"); + return false; + } + break; + } + else + { + nodes.push(node); + } } - } - if (sizeFound == false) - { - if (sheetSizeX == sheetSizeY) + if (nodes.length == now.length) { - sheetSizeX *= 2; + // stop the loop, attempt the 75% check here + index += nodes.length; } else { - sheetSizeY *= 2; + if (sizeX == sizeY) + { + sizeX *= 2; + } + else + { + sizeY *= 2; + } + if (sizeX > 2048 || sizeY > 2048) + { + index += now.length; + } } } + + if (index > startIndex) + { + for (var i = startIndex; i < index; i++) + { + frames[i].sheetIndex = sheets.length; + frames[i].rect = nodes[i - startIndex].Rect; + } + sheets.push(sheet); + } } - return new Point(sheetSizeX, sheetSizeY); + + return output; + } + + private function PackFrames(toPack:Array, sizeX:int, sizeY:int):Array + { + } } } \ No newline at end of file diff --git a/FramePacker.as b/FramePacker.as index 6508f32..8f6f97b 100755 --- a/FramePacker.as +++ b/FramePacker.as @@ -1,91 +1,124 @@ package { + import flash.geom.*; + import flash.display.*; public class FramePacker { + private var sheets:Array; // [RectanglePacker] private var frames:Array; // [FrameInfo] - public void Pack(frames:Array) + public void FramePacker(frames:Array) { this.frames = frames.filter(function(i:FrameInfo) { return i.frame != null; }); } +import Packer.RectanglePacker; +import Packer.RectangleNode; - public function DetermineMinimumSheetSizeForClips(frames:Array):Point + private function GetMaxSize(frames:Array):Point { - - /* TODO - FOR LARGE SIZED CHUNKS BETTER HANDLING FOR FINDING MINIMUM SIZE! */ - - var areas:Array = new Array(); - - for (var i = 0; i < frames.length; i++) + if (frames.length == 0) return new Point(); + return new Point(Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.width; })), + Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.width; }))); + } + + private function GetArea(frames:Array):int + { + var area = 0; + for (var i in frames) { - are + if (frames[i] instanceof FrameInfo) + { + area += frames[i].frame.width * frames[i].frame.height; + } + if (frames[i] instanceof BitmapData || frames[i] instanceof Rectangle) + { + area += frames[i].width * frames[i].height; + } } - var areas:Array = frames.map(function(i:FrameInfo) { return i.frame.width * i.frame.height; }); - var maxW = Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.width; })); - var maxH = Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.height; })); - var max = Math.max(maxW, maxH); - - var sorted:Array = areas.sort(Array.RETURNINDEXEDARRAY | Array.NUMERIC | Array.DESCENDING); - - var exponent = Math.ceil(Math.log(max) / Math.log(2)); - max = Math.pow(2, exponent); - - //rough heuristic to see if this process is worth doing. - //it can be slow if there are a large number of big clips. - if (frames.length > 100 && max >= 512) + return area; + } + + public const AreaUsedMultiplier = 1.3; // guess how much more sheet space we use than actual space taken up by frames + + public function Pack():Boolean + { + sheets = new Array(); + var index = 0; + while (index < frames.length) { - var s = Math.max(max, 1024); - return new Point(s, s); - } + var now:Array = frames.slice(index); - sheets.push(new SpriteSheet(2048, 2048)); - for (var i = 0; i < frames.length; i++) - { - var sprite:TextureSprite = new TextureSprite(clips[sorted[i]], CACHESEQUENCES); - CacheClip(clips[sorted[i]], sprite); - sprites.push(sprite); - } - - var allUsedRects:Array = new Array(); - var nodesToVisit:Array = new Array(); - for (var i = 0; i < sheets.length; i++) - { - var sheet:SpriteSheet = sheets[i]; - var nodes = sheet.GetAllocator().GetLeafNodes(); - for (var n = 0; n < nodes.length; n++) + var max:Point = GetMaxSize(now); + var area:int = GetArea(now); + + var exponent = Math.floor(Math.log(area * AreaUsedMultiplier) / Math.log(2)); + var sizeX:int = Math.pow(2, exponent); + var sizeY:int = startSizeX; + + var sheet:RectanglePacker = new RectanglePacker(sizeX, sizeY); + + var startIndex = index; + while (index < frames.length) { - allUsedRects.push(nodes[n].Rect); - } - } - - var sizeFound = false; - var sheetSizeX = 64; - var sheetSizeY = 64; - while(!sizeFound) - { - var sheet:SpriteSheet = new SpriteSheet(sheetSizeX, sheetSizeY); - sizeFound = true; - for(var i = 0; i < allUsedRects.length; i++) - { - var node:RectangleNode = sheet.GetAllocator().Insert(allUsedRects[i].width, allUsedRects[i].height); - if (node == null) + var done = false; + var nodes:Array = new Array(); + var sorted:Array = now.sortOn(now, Array.RETURNINDEXEDARRAY | Array.DESCENDING, function(a:FrameInfo, b:FrameInfo) { return (a.frame.width * a.frame.height) - (b.frame.width - b.frame.height); }); + for (var sortedI = 0; sortedI < now.length; sortedI++) { - sizeFound = false; - break; + var i = nodes[sorted[sortedI]]; + var node:RectangleNode = sheet.Insert(now[i].frame.width, now[i].frame.height); + if (node == null) + { + if (sortedI == 0) + { + Exporter.Instance.Print("Failure: item too large to pack on sheet"); + return false; + } + break; + } + else + { + nodes.push(node); + } } - } - if (sizeFound == false) - { - if (sheetSizeX == sheetSizeY) + if (nodes.length == now.length) { - sheetSizeX *= 2; + // stop the loop, attempt the 75% check here + index += nodes.length; } else { - sheetSizeY *= 2; + if (sizeX == sizeY) + { + sizeX *= 2; + } + else + { + sizeY *= 2; + } + if (sizeX > 2048 || sizeY > 2048) + { + index += now.length; + } } } + + if (index > startIndex) + { + for (var i = startIndex; i < index; i++) + { + frames[i].sheetIndex = sheets.length; + frames[i].rect = nodes[i - startIndex].Rect; + } + sheets.push(sheet); + } } - return new Point(sheetSizeX, sheetSizeY); + + return output; + } + + private function PackFrames(toPack:Array, sizeX:int, sizeY:int):Array + { + } } } \ No newline at end of file diff --git a/RectangleNode.as b/RectangleNode.as new file mode 100755 index 0000000..a6b0c74 --- /dev/null +++ b/RectangleNode.as @@ -0,0 +1,178 @@ +package { + import flash.geom.Rectangle; + + public class RectangleNode + { + public var Left:RectangleNode = null; + public var Right:RectangleNode = null; + public var Rect:Rectangle = null; + public var InUse:Boolean = false; + public var Removed:Boolean = false; + public var Parent:RectangleNode = null; + public var AllocationID = 0; + public var Host:RectanglePacker= null; + public var MinAllocationID = 0; + public var NodeID; + public static var NextNodeID = 0; + + public var DontRemove = false; + + public var LastAccessTime = 0; + + public var Flags = []; + + public var UpdateTo:RectangleNode = null; + + /* + public function get Removed() + { + return _Removed; + } + + public function set Removed(r) + { + this._Removed = r; + } + + public function get Host():RectanglePacker + { + return _Host; + } + public function set Host(p:RectanglePacker) + { + this._Host = p; + + if (_Left && _Left.Host != p) + { + throw new Error("bad left host"); + } + if (_Right && _Right.Host != p) + { + throw new Error("bad right host"); + } + if (_Parent && _Parent.Host != p) + { + throw new Error("bad parent host"); + } + + } + + public function get Left():RectangleNode + { + return _Left; + } + public function set Left(n:RectangleNode) + { + if (n && n.Host != Host) + { + throw new Error("bad left host"); + } + _Left = n; + } + public function get Right():RectangleNode + { + return _Right; + } + public function set Right(n:RectangleNode) + { + if (n && n.Host != Host) + { + throw new Error("bad right host"); + } + _Right = n; + } + + public function get Parent():RectangleNode + { + return _Parent; + } + public function set Parent(n:RectangleNode) + { + if (n && n.Host != Host) + { + throw new Error("bad parent host"); + } + _Parent = n; + } + */ + public function get IsFreeSpace() + { + return Left == null && Right == null && !InUse; + } + public function UpdateLinksFromNode(node:RectangleNode) + { + if (node.Parent) + { + if (node.Parent.Left == node) + { + node.Parent.Left = this; + } + if (node.Parent.Right == node) + { + node.Parent.Right = this; + } + } + if (node.Left) + { + node.Left.Parent = this; + } + if (node.Right) + { + node.Right.Parent = this; + } + } + public function MakeFromNode(node:RectangleNode) + { + //_Host = node.Host; + Host = node.Host; + Left = node.Left; + Right = node.Right; + Parent = node.Parent; + AllocationID = node.AllocationID; + InUse = node.InUse; + MinAllocationID = node.MinAllocationID; + NodeID = node.NodeID; + Rect = node.Rect.clone(); + Removed = node.Removed; + DontRemove = node.DontRemove; + } + public function RectangleNode(x, y, w, h, host:RectanglePacker) + { + this.NodeID = NextNodeID++; + this.Host = host; + this.Rect = new Rectangle(x, y, w, h); + } + public function OutputJSON() + { + var node = {}; + node.rect = [this.Rect.left, this.Rect.top, this.Rect.width, this.Rect.height]; + node.in_use = InUse; + node.id = NodeID; + if (this.Left) node.left = this.Left.OutputJSON(); + if (this.Right) node.right = this.Right.OutputJSON(); + return node; + } + + public function ReadJSON(details) + { + details.new_node = this; + this.Rect = new Rectangle(details.rect[0], details.rect[1], details.rect[2], details.rect[3]); + this.Removed = false; + + this.InUse = details.in_use; + + if (details.hasOwnProperty("right")) + { + this.Right = new RectangleNode(0, 0, 0, 0, this.Host); + Right.ReadJSON(details.right); + Right.Parent = this; + } + if (details.hasOwnProperty("left")) + { + this.Left = new RectangleNode(0, 0, 0, 0, this.Host); + Left.ReadJSON(details.left); + Left.Parent = this; + } + } + } +} \ No newline at end of file diff --git a/FramePacker.as b/FramePacker.as index 6508f32..8f6f97b 100755 --- a/FramePacker.as +++ b/FramePacker.as @@ -1,91 +1,124 @@ package { + import flash.geom.*; + import flash.display.*; public class FramePacker { + private var sheets:Array; // [RectanglePacker] private var frames:Array; // [FrameInfo] - public void Pack(frames:Array) + public void FramePacker(frames:Array) { this.frames = frames.filter(function(i:FrameInfo) { return i.frame != null; }); } +import Packer.RectanglePacker; +import Packer.RectangleNode; - public function DetermineMinimumSheetSizeForClips(frames:Array):Point + private function GetMaxSize(frames:Array):Point { - - /* TODO - FOR LARGE SIZED CHUNKS BETTER HANDLING FOR FINDING MINIMUM SIZE! */ - - var areas:Array = new Array(); - - for (var i = 0; i < frames.length; i++) + if (frames.length == 0) return new Point(); + return new Point(Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.width; })), + Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.width; }))); + } + + private function GetArea(frames:Array):int + { + var area = 0; + for (var i in frames) { - are + if (frames[i] instanceof FrameInfo) + { + area += frames[i].frame.width * frames[i].frame.height; + } + if (frames[i] instanceof BitmapData || frames[i] instanceof Rectangle) + { + area += frames[i].width * frames[i].height; + } } - var areas:Array = frames.map(function(i:FrameInfo) { return i.frame.width * i.frame.height; }); - var maxW = Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.width; })); - var maxH = Math.max.apply(Math, frames.map(function(i:FrameInfo) { return i.frame.height; })); - var max = Math.max(maxW, maxH); - - var sorted:Array = areas.sort(Array.RETURNINDEXEDARRAY | Array.NUMERIC | Array.DESCENDING); - - var exponent = Math.ceil(Math.log(max) / Math.log(2)); - max = Math.pow(2, exponent); - - //rough heuristic to see if this process is worth doing. - //it can be slow if there are a large number of big clips. - if (frames.length > 100 && max >= 512) + return area; + } + + public const AreaUsedMultiplier = 1.3; // guess how much more sheet space we use than actual space taken up by frames + + public function Pack():Boolean + { + sheets = new Array(); + var index = 0; + while (index < frames.length) { - var s = Math.max(max, 1024); - return new Point(s, s); - } + var now:Array = frames.slice(index); - sheets.push(new SpriteSheet(2048, 2048)); - for (var i = 0; i < frames.length; i++) - { - var sprite:TextureSprite = new TextureSprite(clips[sorted[i]], CACHESEQUENCES); - CacheClip(clips[sorted[i]], sprite); - sprites.push(sprite); - } - - var allUsedRects:Array = new Array(); - var nodesToVisit:Array = new Array(); - for (var i = 0; i < sheets.length; i++) - { - var sheet:SpriteSheet = sheets[i]; - var nodes = sheet.GetAllocator().GetLeafNodes(); - for (var n = 0; n < nodes.length; n++) + var max:Point = GetMaxSize(now); + var area:int = GetArea(now); + + var exponent = Math.floor(Math.log(area * AreaUsedMultiplier) / Math.log(2)); + var sizeX:int = Math.pow(2, exponent); + var sizeY:int = startSizeX; + + var sheet:RectanglePacker = new RectanglePacker(sizeX, sizeY); + + var startIndex = index; + while (index < frames.length) { - allUsedRects.push(nodes[n].Rect); - } - } - - var sizeFound = false; - var sheetSizeX = 64; - var sheetSizeY = 64; - while(!sizeFound) - { - var sheet:SpriteSheet = new SpriteSheet(sheetSizeX, sheetSizeY); - sizeFound = true; - for(var i = 0; i < allUsedRects.length; i++) - { - var node:RectangleNode = sheet.GetAllocator().Insert(allUsedRects[i].width, allUsedRects[i].height); - if (node == null) + var done = false; + var nodes:Array = new Array(); + var sorted:Array = now.sortOn(now, Array.RETURNINDEXEDARRAY | Array.DESCENDING, function(a:FrameInfo, b:FrameInfo) { return (a.frame.width * a.frame.height) - (b.frame.width - b.frame.height); }); + for (var sortedI = 0; sortedI < now.length; sortedI++) { - sizeFound = false; - break; + var i = nodes[sorted[sortedI]]; + var node:RectangleNode = sheet.Insert(now[i].frame.width, now[i].frame.height); + if (node == null) + { + if (sortedI == 0) + { + Exporter.Instance.Print("Failure: item too large to pack on sheet"); + return false; + } + break; + } + else + { + nodes.push(node); + } } - } - if (sizeFound == false) - { - if (sheetSizeX == sheetSizeY) + if (nodes.length == now.length) { - sheetSizeX *= 2; + // stop the loop, attempt the 75% check here + index += nodes.length; } else { - sheetSizeY *= 2; + if (sizeX == sizeY) + { + sizeX *= 2; + } + else + { + sizeY *= 2; + } + if (sizeX > 2048 || sizeY > 2048) + { + index += now.length; + } } } + + if (index > startIndex) + { + for (var i = startIndex; i < index; i++) + { + frames[i].sheetIndex = sheets.length; + frames[i].rect = nodes[i - startIndex].Rect; + } + sheets.push(sheet); + } } - return new Point(sheetSizeX, sheetSizeY); + + return output; + } + + private function PackFrames(toPack:Array, sizeX:int, sizeY:int):Array + { + } } } \ No newline at end of file diff --git a/RectangleNode.as b/RectangleNode.as new file mode 100755 index 0000000..a6b0c74 --- /dev/null +++ b/RectangleNode.as @@ -0,0 +1,178 @@ +package { + import flash.geom.Rectangle; + + public class RectangleNode + { + public var Left:RectangleNode = null; + public var Right:RectangleNode = null; + public var Rect:Rectangle = null; + public var InUse:Boolean = false; + public var Removed:Boolean = false; + public var Parent:RectangleNode = null; + public var AllocationID = 0; + public var Host:RectanglePacker= null; + public var MinAllocationID = 0; + public var NodeID; + public static var NextNodeID = 0; + + public var DontRemove = false; + + public var LastAccessTime = 0; + + public var Flags = []; + + public var UpdateTo:RectangleNode = null; + + /* + public function get Removed() + { + return _Removed; + } + + public function set Removed(r) + { + this._Removed = r; + } + + public function get Host():RectanglePacker + { + return _Host; + } + public function set Host(p:RectanglePacker) + { + this._Host = p; + + if (_Left && _Left.Host != p) + { + throw new Error("bad left host"); + } + if (_Right && _Right.Host != p) + { + throw new Error("bad right host"); + } + if (_Parent && _Parent.Host != p) + { + throw new Error("bad parent host"); + } + + } + + public function get Left():RectangleNode + { + return _Left; + } + public function set Left(n:RectangleNode) + { + if (n && n.Host != Host) + { + throw new Error("bad left host"); + } + _Left = n; + } + public function get Right():RectangleNode + { + return _Right; + } + public function set Right(n:RectangleNode) + { + if (n && n.Host != Host) + { + throw new Error("bad right host"); + } + _Right = n; + } + + public function get Parent():RectangleNode + { + return _Parent; + } + public function set Parent(n:RectangleNode) + { + if (n && n.Host != Host) + { + throw new Error("bad parent host"); + } + _Parent = n; + } + */ + public function get IsFreeSpace() + { + return Left == null && Right == null && !InUse; + } + public function UpdateLinksFromNode(node:RectangleNode) + { + if (node.Parent) + { + if (node.Parent.Left == node) + { + node.Parent.Left = this; + } + if (node.Parent.Right == node) + { + node.Parent.Right = this; + } + } + if (node.Left) + { + node.Left.Parent = this; + } + if (node.Right) + { + node.Right.Parent = this; + } + } + public function MakeFromNode(node:RectangleNode) + { + //_Host = node.Host; + Host = node.Host; + Left = node.Left; + Right = node.Right; + Parent = node.Parent; + AllocationID = node.AllocationID; + InUse = node.InUse; + MinAllocationID = node.MinAllocationID; + NodeID = node.NodeID; + Rect = node.Rect.clone(); + Removed = node.Removed; + DontRemove = node.DontRemove; + } + public function RectangleNode(x, y, w, h, host:RectanglePacker) + { + this.NodeID = NextNodeID++; + this.Host = host; + this.Rect = new Rectangle(x, y, w, h); + } + public function OutputJSON() + { + var node = {}; + node.rect = [this.Rect.left, this.Rect.top, this.Rect.width, this.Rect.height]; + node.in_use = InUse; + node.id = NodeID; + if (this.Left) node.left = this.Left.OutputJSON(); + if (this.Right) node.right = this.Right.OutputJSON(); + return node; + } + + public function ReadJSON(details) + { + details.new_node = this; + this.Rect = new Rectangle(details.rect[0], details.rect[1], details.rect[2], details.rect[3]); + this.Removed = false; + + this.InUse = details.in_use; + + if (details.hasOwnProperty("right")) + { + this.Right = new RectangleNode(0, 0, 0, 0, this.Host); + Right.ReadJSON(details.right); + Right.Parent = this; + } + if (details.hasOwnProperty("left")) + { + this.Left = new RectangleNode(0, 0, 0, 0, this.Host); + Left.ReadJSON(details.left); + Left.Parent = this; + } + } + } +} \ No newline at end of file diff --git a/RectanglePacker.as b/RectanglePacker.as new file mode 100755 index 0000000..be87439 --- /dev/null +++ b/RectanglePacker.as @@ -0,0 +1,560 @@ +package { + import flash.geom.*; + import flash.display.BitmapData; + import flash.display.MovieClip; + import flash.text.TextField; + import flash.events.*; + import flash.sampler.StackFrame; + + public class RectanglePacker + { + protected var w; + protected var h; + protected var root:RectangleNode; + public var NextAllocationID = 0; + + public function RectanglePacker(w, h) + { + this.w = w; + this.h = h; + root = new RectangleNode(0, 0, w, h, this); + root.AllocationID = -1; + } + protected var area = 0; + + protected var testRender:MovieClip = new MovieClip(); + + public function GetRoot():RectangleNode + { + return root; + } + + public function GetLeafNodes():Array + { + return GetLeafNodesInternal(root); + } + + protected function GetLeafNodesInternal(n:RectangleNode):Array + { + if (n.InUse) return [n]; + var ret:Array = new Array(); + if (n.Left) ret = GetLeafNodesInternal(n.Left); + if (n.Right) ret = ret.concat(GetLeafNodesInternal(n.Right)); + return ret; + } + + public function GetUsedArea() + { + return area; + } + + public function get efficiency() + { + return area / (w * h); + } + + protected function RectCost(w, h) + { + if (w == 0 || h == 0) return 99999999; + return (Math.max(w, h) / Math.min(w, h)) * w * h; + } + + protected function EnumerateTree(r:RectangleNode = null, indent = "") + { + if (r == null) r = this.root; + var str = ""; + str += indent + r.NodeID + " r:" + r.Removed + " iu" + r.InUse + " ifs" + r.IsFreeSpace + " " + r.Rect.left + "," + r.Rect.top + " -> " + r.Rect.right + "," + r.Rect.bottom + "\n"; + if (r.Left) str += EnumerateTree(r.Left, indent + " "); + if (r.Right) str += EnumerateTree(r.Left, indent + " "); + return str; + } + + /* + * Remove a list of nodes from the tree. + * - fullyDestroy: if not set, the nodes will be reused as nodes representing the new empty space in the tree + * if set, the nodes will be duplicated as new objects to be inserted into the tree as blank space, + leaving the original objects intact (in case something still references them) + */ + public function RemoveNodes(nodes:Array, fullyDestroy = true) + { + + /* DEBUG CHECKS */ + /* + this.PerformDebugCheck(); + + var str = ""; + for (var i = 0; i < nodes.length; i++) + { + str += nodes[i].NodeID + "\n"; + } + + var orig = nodes.concat(); + + var strs = [str, EnumerateTree()]; + + RenderDetailsBox.StartTrack("PackRemoveTime"); + + for (var i = 0; i < nodes.length; i++) + { + var n:RectangleNode = nodes[i]; + if (n.Host != this) + { + throw new Error("bad host!"); + } + } + */ + /* END DEBUG CHECKS */ + + // setup the Removed flag on every node that should be removed + // we also check at this time to see if any more nodes need to be removed, such as a parent of + // 2 removed nodes (we want to optimize the tree to get rid of completely empty subtrees) + for (var i = 0; i < nodes.length; i++) + { + var n:RectangleNode = nodes[i]; + if (n.Host == this) + { + n.Removed = true; + + if (n.InUse) + { + // sum up the total removed area + area -= n.Rect.width * n.Rect.height; + } + + if (n.Parent != null) + { + var p:RectangleNode = n.Parent; + // check and see if the current node's parent still has any valid children + if ((p.Left.IsFreeSpace || p.Left.Removed) && (p.Right.IsFreeSpace || p.Right.Removed)) + { + // all of the current nodes siblings are either being removed or are blank, so + // we can remove the parent too + if (p.Left.IsFreeSpace) + { + // the left child was empty space before, so we can remove it (if it wasn't + // empty space it is already being removed so it doesn't need to be added + // to the list) + if (nodes.indexOf(p.Left) == -1) nodes.push(p.Left); + } + if (p.Right.IsFreeSpace) + { + // the right child was empty space before, so we can remove it + if (nodes.indexOf(p.Right) == -1) nodes.push(p.Right); + } + if (p != root && nodes.indexOf(p) == -1) + { + // remove the parent node too (provided it isn't the root!) + nodes.push(p); + } + } + } + } + } + + // now we have an expanded list of all nodes that will actually be cut out to return an optimized tree, + // so go ahead and remove them + for (var i = 0; i < nodes.length; i++) + { + var n:RectangleNode = nodes[i]; + + if (n.Parent != null) + { + // n is getting removed, so n.Parent may have some changes to its pointers (provided n.Parent is not + // also getting removed!) + var p:RectangleNode = n.Parent; + + if (!p.Removed) + { + if (p.Left && p.Right) + { + if (p.Left.Removed && p.Right.Removed) + { + // both children were removed, but the parent wasn't, so the parent must be root + // (since we never remove the root) + if (p != root) + { + throw new Error("should be root"); + } + p.Left.Parent = null; + p.Right.Parent = null; + p.Left = null; + p.Right = null; + p.InUse = false; + } else if (!p.Left.Removed) { + // Right node removed, Left node not removed + if (!p.Left.IsFreeSpace) + { + if (fullyDestroy) + { + // duplicate the node representing empty space so the original node + // remains untouched + var newNode:RectangleNode = new RectangleNode(0, 0, 0, 0, this); + newNode.MakeFromNode(p.Right); + newNode.UpdateLinksFromNode(p.Right); + } + // Left node is used + p.Right.InUse = false; + p.Right.Left = null; + p.Right.Right = null; + p.Right.Removed = false; + } else { + throw new Error("should have been caught above!"); + } + } else { + // Left node removed, Right node not removed + if (!p.Right.IsFreeSpace) + { + if (fullyDestroy) + { + // duplicate the node representing empty space so the original node + // remains untouched + var newNode:RectangleNode = new RectangleNode(0, 0, 0, 0, this); + newNode.MakeFromNode(p.Left); + newNode.UpdateLinksFromNode(p.Left); + } + // Left node is used + //p.Left.Removed = true; + p.Left.InUse = false; + p.Left.Left = null; + p.Left.Right = null; + p.Left.Removed = false; + //p.Left.Parent = null; + } else { + throw new Error("should have been caught above!"); + } + } + } else { + // huh? how does n.Parent == p but p has no children? something went wrong + throw new Error("shouldn't have gotten here"); + } + } + } + //strs.push(EnumerateTree()); + } + RenderDetailsBox.EndTrack("PackRemoveTime"); + } + + public function Insert(w, h, useInstead:RectangleNode = null):RectangleNode + { + + var node:RectangleNode = FindSizedNode(w, h, root); + + if (node == null) + { + //UHOH, texture full + return null; + } + area += w * h; + node.InUse = true; + + RenderDetailsBox.StartTrack("PackInsertTime"); + var splitVertFirstCost = 0; + if (w < node.Rect.width) + { + splitVertFirstCost = RectCost(node.Rect.width - w, node.Rect.height) + RectCost(w, node.Rect.height - h); + } + + var splitHorizFirstCost = 0; + if (h < node.Rect.height) + { + splitHorizFirstCost = RectCost(node.Rect.width - w, h) + RectCost(node.Rect.width, node.Rect.height - h); + } + + + + if (splitVertFirstCost <= splitHorizFirstCost) + { + node.Flags.push("SplitVertFirst"); + if (w < node.Rect.width) + { + node.Flags.push("SplitVert"); + node.InUse = false; + // split node vertically + SplitVert(node, w); + node.Left.InUse = true; + node = node.Left; + } + if (h < node.Rect.height) + { + node.Flags.push("SplitHorz"); + node.InUse = false; + SplitHoriz(node, h); + node.Left.InUse = true; + node = node.Left; + } + } else { + node.Flags.push("SplitHorzFirst"); + if (h < node.Rect.height) + { + node.Flags.push("SplitVert"); + node.InUse = false; + SplitHoriz(node, h); + node.Left.InUse = true; + node = node.Left; + } + if (w < node.Rect.width) + { + node.Flags.push("SplitHorz"); + node.InUse = false; + // split node vertically + SplitVert(node, w); + node.Left.InUse = true; + node = node.Left; + } + } + NextAllocationID++; + if (useInstead) + { + useInstead.MakeFromNode(node); + useInstead.UpdateLinksFromNode(node); + useInstead.Flags = node.Flags; + node = useInstead; + node.Flags.push("UseInstead"); + } + + RenderDetailsBox.EndTrack("PackInsertTime"); + + //if (!CheckOKNode(node)) node.Flags.push("BadNode"); + + return node; + } + public function CheckOKNode(n:RectangleNode, r:RectangleNode = null) + { + if (r == null) r = this.root; + if (n != r && r.InUse && n.Rect.intersection(r.Rect).size.length > 0) + { + var ancestor:RectangleNode = FindCommonNode(n, r); + throw new Error("bad node allocated!"); + return false; + } + var ok = true; + if (r.Left) ok = ok && CheckOKNode(n, r.Left); + if (r.Right) ok = ok && CheckOKNode(n, r.Right); + return ok; + } + + public function PerformDebugCheck() + { + DebugInternal(this.root); + } + + protected function DebugInternal(n:RectangleNode) + { + if (n.Host != this) + { + throw new Error("problem with host!"); + } + if (n.IsFreeSpace) + { + if (n.Left || n.Right) + { + throw new Error("shouldn't have children"); + } + } + if ((n.Left && !n.Right) || (n.Right && !n.Left)) + { + throw new Error("mismatched children"); + } + if (n.Left && n.Left.Parent != n) + { + throw new Error("left child has bad parent"); + } + if (n.Right && n.Right.Parent != n) + { + throw new Error("right child has bad parent"); + } + if (n.Parent && n.Parent.Left != n && n.Parent.Right != n) + { + throw new Error("problem with parent"); + } + if (n.Left) DebugInternal(n.Left); + if (n.Right) DebugInternal(n.Right); + } + + public function FindCommonNode(a:RectangleNode, b:RectangleNode) + { + var aHistory = []; + var bHistory = []; + while (a != null) + { + aHistory.push(a); + a = a.Parent; + } + while (b != null) + { + bHistory.push(b); + b = b.Parent; + } + var i = aHistory.length - 1; + var j = bHistory.length - 1; + var same = null; + while (i >= 0 && j >= 0) + { + if (aHistory[i] == bHistory[j]) + { + same = aHistory[i]; + } else { + break; + } + i--; + j--; + } + return same; + } + + public function TestRender() + { + while (testRender.numChildren > 0) + { + testRender.removeChildAt(0); + } + testRender.graphics.clear(); + TestRenderNode(root, testRender); + return testRender; + } + protected function TestRenderNode(n:RectangleNode, m:MovieClip) + { + var g:MovieClip = new MovieClip(); + m.addChild(g); + g.graphics.lineStyle(1, 0, 1); + if (n.Left != null) TestRenderNode(n.Left, m); + if (n.Left != null) TestRenderNode(n.Right, m); + if (n.InUse) + { + g.graphics.beginFill(0xFFFF00, 0.2); + } + g.graphics.drawRect(n.Rect.x, n.Rect.y, n.Rect.width, n.Rect.height); + if (n.InUse) + { + g.graphics.endFill(); + var t:TextField = new TextField(); + t.text = n.NodeID; + t.x = n.Rect.x + n.Rect.width / 2.0; + t.y = n.Rect.y + n.Rect.height / 2.0; + g.addChild(t); + g.addEventListener(MouseEvent.MOUSE_OVER, function(e){MouseOver(n,g,t);}); + g.addEventListener(MouseEvent.MOUSE_OUT, function(e){MouseOut(n,g,t);}); + } + + } + public function MouseOver(n:RectangleNode, g:MovieClip, t:TextField) + { + if (n.InUse) + { + _trace(Math.round(n.LastAccessTime / 1000)); + } + var p:RectangleNode = n.Parent; + if (p) + { + g.graphics.clear(); + g.graphics.lineStyle(1, 0, 1); + g.graphics.beginFill(p.IsFreeSpace ? 0x0000FF : 0xFF0000, 0.4); + g.graphics.drawRect(p.Rect.x, p.Rect.y, p.Rect.width, p.Rect.height); + g.graphics.endFill(); + t.text = p.NodeID; + if (g.oldListener) g.removeEventListener(MouseEvent.CLICK, g.oldListener); + g.oldListener = function(e){MouseOver(p, g, t);}; + g.addEventListener(MouseEvent.CLICK, g.oldListener); + } + } + public function MouseOut(n:RectangleNode, g:MovieClip, t:TextField) + { + g.graphics.clear(); + g.graphics.lineStyle(1, 0, 1); + g.graphics.beginFill(0xFFFF00, 0.2); + g.graphics.drawRect(n.Rect.x, n.Rect.y, n.Rect.width, n.Rect.height); + g.graphics.endFill(); + t.text = n.NodeID; + } + protected function SplitVert(node:RectangleNode, leftWidth) + { + node.Left = new RectangleNode(node.Rect.x, node.Rect.y, leftWidth, node.Rect.height, this); + node.Left.AllocationID = NextAllocationID; + node.Left.Parent = node; + node.Right = new RectangleNode(node.Rect.x + leftWidth, node.Rect.y, node.Rect.width - leftWidth, node.Rect.height, this); + node.Right.AllocationID = NextAllocationID; + node.Right.Parent = node; + + //CheckOKNode(node.Left); + //CheckOKNode(node.Right); + } + protected function SplitHoriz(node:RectangleNode, topHeight) + { + node.Left = new RectangleNode(node.Rect.x, node.Rect.y, node.Rect.width, topHeight, this); + node.Left.AllocationID = NextAllocationID; + node.Left.Parent = node; + node.Right = new RectangleNode(node.Rect.x, node.Rect.y + topHeight, node.Rect.width, node.Rect.height - topHeight, this); + node.Right.AllocationID = NextAllocationID; + node.Right.Parent = node; + + //CheckOKNode(node.Left); + //CheckOKNode(node.Right); + } + protected function FindSizedNode(w, h, node:RectangleNode):RectangleNode + { + if (node.InUse) return null; + if (node.Rect.width >= w && node.Rect.height >= h) + { + if (node.Left != null && node.Right != null) + { + var t:RectangleNode = FindSizedNode(w, h, node.Left); + if (t != null) return t; + t = FindSizedNode(w, h, node.Right); + return t; + } else { + return node; + } + } + return null; + } + + public function LoadTree(details) + { + this.root = new RectangleNode(0, 0, 0, 0, this); + root.ReadJSON(details); + + this.NextAllocationID = 0; + this.area = 0; + + var queue:Array = new Array(); + queue.push(root); + while (queue.length > 0) + { + var node:RectangleNode = queue.shift(); + node.AllocationID = NextAllocationID; + node.MinAllocationID = NextAllocationID; + if (node.Left) queue.push(node.Left); + if (node.Right) queue.push(node.Right); + if (node.InUse) this.area += node.Rect.width * node.Rect.height; + } + NextAllocationID++; + } + + public function GetNodePath(node:RectangleNode) + { + var output:String = ""; + while (node != this.root) + { + output = (node == node.Parent.Left ? "l" : "r") + output; + node = node.Parent; + } + return output; + } + + public function GetNodeFromPath(path:String):RectangleNode + { + var node:RectangleNode = this.root; + for (var i = 0; i < path.length; i++) + { + if (!node) return null; + if (path.charAt(i) == "r") + { + node = node.Right; + } else { + node = node.Left; + } + } + return node; + } + } +} \ No newline at end of file