Newer
Older
exporter / RectanglePacker.as
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;
		}
	}
}