2021SC@SDUSC
1, Overview
This paper will mainly describe the related methods in packing package in ebiten. Packing package provides a two-dimensional space packing algorithm. The file is located in the internal/packing/packing.go folder.
2, Code analysis
const ( minSize = 1 ) type Page struct { root *Node size int maxSize int rollbackExtension func() } func NewPage(initSize int, maxSize int) *Page { return &Page{ size: initSize, maxSize: maxSize, } } func (p *Page) IsEmpty() bool { if p.root == nil { return true } return !p.root.used && p.root.child0 == nil && p.root.child1 == nil } type Node struct { x int y int width int height int used bool parent *Node child0 *Node child1 *Node }
First, define a page class with three attributes: root node, current size and maximum capacity, and a rollback extension method. Then, create a new page method and judge whether the page is empty.
Next, the Node node class corresponding to the root attribute in the page class is defined.
There are x,y,width,height,used,parent,child1,child0 attributes.
Then is the method to judge whether the Node can be released:
func (n *Node) canFree() bool { if n.used { return false } if n.child0 == nil && n.child1 == nil { return true } return n.child0.canFree() && n.child1.canFree() } func (n *Node) Region() (x, y, width, height int) { return n.x, n.y, n.width, n.height }
If the used attribute of the node is true, it cannot be released. If both child nodes of the node are empty, it can be released. If the above conditions are not met, both bytes of the node can be released, and it can also be released itself.
The Region method is to obtain the X, y, width and height attributes of the Node.
Next are the square method and the alloc method:
//Square returns a floating-point value representing the distance between a given rectangle and a square. //Returns 1 (maximum) if the given rectangle is a square. //Otherwise, the value in [0, 1] is returned. func square(width, height int) float64 { if width == 0 && height == 0 { return 0 } if width <= height { return float64(width) / float64(height) } return float64(height) / float64(width) } func (p *Page) alloc(n *Node, width, height int) *Node { if n.width < width || n.height < height { return nil } if n.used { return nil } if n.child0 == nil && n.child1 == nil { if n.width == width && n.height == height { n.used = true return n } if square(n.width-width, n.height) >= square(n.width, n.height-height) { //Split Vertically n.child0 = &Node{ x: n.x, y: n.y, width: width, height: n.height, parent: n, } n.child1 = &Node{ x: n.x + width, y: n.y, width: n.width - width, height: n.height, parent: n, } } else { //Overall split n.child0 = &Node{ x: n.x, y: n.y, width: n.width, height: height, parent: n, } n.child1 = &Node{ x: n.x, y: n.y + height, width: n.width, height: n.height - height, parent: n, } } return p.alloc(n.child0, width, height) } if n.child0 == nil || n.child1 == nil { panic("packing: both two children must not be nil at alloc") } if node := p.alloc(n.child0, width, height); node != nil { return node } if node := p.alloc(n.child1, width, height); node != nil { return node } return nil }
The square method obtains the gap between the rectangle and the square according to the width and height of the Node. If the width and height are all 0, it returns 0. Otherwise, it returns the float type result of dividing the shorter edge by the longer edge.
Alloc method is a recursive method. The incoming parameters are node, width and height. First, judge and compare the width and incoming width of the node, the height of the node and the incoming height. If the node attribute is small, nil is returned. If the incoming node has been used, nil is also returned. Next, judge if the two child nodes of the incoming node are empty, Decide whether to perform vertical split or overall split according to the difference between the width and height of the node and the incoming width and height, and then call this method for recursion; If the child nodes are not empty at the same time, the value of calling the alloc method on the child node that is not empty is returned.
Then the size method and the SetMaxSize method:
func (p *Page) Size() int { return p.size } func (p *Page) SetMaxSize(size int) { if p.maxSize > size { panic("packing: maxSize cannot be decreased") } p.maxSize = size }
Obtain the current size of the page object and set the maximum size of the page object.
Next, the Alloc method and Free method of the page class are defined:
func (p *Page) Alloc(width, height int) *Node { if width <= 0 || height <= 0 { panic("packing: width and height must > 0") } if p.root == nil { p.root = &Node{ width: p.size, height: p.size, } } if width < minSize { width = minSize } if height < minSize { height = minSize } n := p.alloc(p.root, width, height) return n } func (p *Page) Free(node *Node) { if node.child0 != nil || node.child1 != nil { panic("packing: can't free the node including children") } node.used = false if node.parent == nil { return } if node.parent.child0 == nil || node.parent.child1 == nil { panic("packing: both two children must not be nil at Free: double free happened?") } if node.parent.child0.canFree() && node.parent.child1.canFree() { node.parent.child0 = nil node.parent.child1 = nil p.Free(node.parent) } }
The alloc method encapsulates the alloc method just now so that the page object can be called. I won't repeat it here.
The Free method first determines whether all the child nodes of the incoming node are empty. If not, an error will pop up. Next, set the used attribute of the node to false. If the parent node of the current node is empty, the method will be terminated between. On the contrary, it determines whether there is an empty node in the child node of the parent node of the current node. If both child nodes exist, Then go to the next step to judge whether both child nodes can be released. If both can be released, set both child nodes of the parent node of the current node to null and try to release the parent node.
Then the walk method:
func walk(n *Node, f func(n *Node) error) error { if err := f(n); err != nil { return err } if n.child0 != nil { if err := walk(n.child0, f); err != nil { return err } } if n.child1 != nil { if err := walk(n.child1, f); err != nil { return err } } return nil }
The function of the walk method is to pass in a node and a method, traverse the two child nodes of the method, and give priority to the child node 0. If it is not empty, you can execute the method and return the error result.
The following are the Extend extension method and the one-time rollback call method, that is, the submit extension method:
func (p *Page) Extend(count int) bool { if p.rollbackExtension != nil { panic("packing: Extend cannot be called without rolling back or committing") } if p.size >= p.maxSize { return false } newSize := p.size for i := 0; i < count; i++ { newSize *= 2 } if newSize > p.maxSize { return false } edgeNodes := []*Node{} abort := errors.New("abort") aborted := false if p.root != nil { _ = walk(p.root, func(n *Node) error { if n.x+n.width < p.size && n.y+n.height < p.size { return nil } if n.used { aborted = true return abort } edgeNodes = append(edgeNodes, n) return nil }) } if aborted { origRoot := *p.root leftUpper := p.root leftLower := &Node{ x: 0, y: p.size, width: p.size, height: newSize - p.size, } left := &Node{ x: 0, y: 0, width: p.size, height: p.size, child0: leftUpper, child1: leftLower, } leftUpper.parent = left leftLower.parent = left right := &Node{ x: p.size, y: 0, width: newSize - p.size, height: newSize, } p.root = &Node{ x: 0, y: 0, width: newSize, height: newSize, child0: left, child1: right, } left.parent = p.root right.parent = p.root origSize := p.size p.rollbackExtension = func() { p.size = origSize p.root = &origRoot } } else { origSize := p.size origWidths := map[*Node]int{} origHeights := map[*Node]int{} for _, n := range edgeNodes { if n.x+n.width == p.size { origWidths[n] = n.width n.width += newSize - p.size } if n.y+n.height == p.size { origHeights[n] = n.height n.height += newSize - p.size } } p.rollbackExtension = func() { p.size = origSize for n, w := range origWidths { n.width = w } for n, h := range origHeights { n.height = h } } } p.size = newSize return true } //RollbackExtension rolls back the extension call once func (p *Page) RollbackExtension() { if p.rollbackExtension == nil { panic("packing: RollbackExtension cannot be called without Extend") } p.rollbackExtension() p.rollbackExtension = nil } //The Committee Extension submits the Extension call. func (p *Page) CommitExtension() { if p.rollbackExtension == nil { panic("packing: RollbackExtension cannot be called without Extend") } p.rollbackExtension = nil }
The pass in parameter of the Extend method is the number of extensions. Continuously expand the size of the newSize. At the same time, note that the newSize cannot be larger than maxSize, and then update the root attribute according to the newSize played by the extension.
The rollback extension method is called once when the rollbackExtension attribute of page is not empty, and set it to empty after calling.
The submission extension method determines that when the rollbackExtension attribute of page is not empty, it will be set to empty.