From b88b86e0e7e51936bdd9b64498d43b2157558edc Mon Sep 17 00:00:00 2001 From: Hajime Hoshi Date: Sun, 25 Feb 2018 20:16:27 +0900 Subject: [PATCH] Add internal/bsp (#514) --- internal/bsp/bsp.go | 148 ++++++++++++++++++++++++++++ internal/bsp/bsp_test.go | 208 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 356 insertions(+) create mode 100644 internal/bsp/bsp.go create mode 100644 internal/bsp/bsp_test.go diff --git a/internal/bsp/bsp.go b/internal/bsp/bsp.go new file mode 100644 index 000000000..915568e44 --- /dev/null +++ b/internal/bsp/bsp.go @@ -0,0 +1,148 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package bsp offers binary space partitioning algorithm. +package bsp + +const ( + MaxSize = 1024 + minSize = 4 +) + +type Page struct { + root *Node +} + +type Node struct { + x int + y int + width int + height int + + used bool + parent *Node + child0 *Node + child1 *Node +} + +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 +} + +func (n *Node) alloc(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 n.height == height || (n.width != width && width <= 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, + } + return n.child0.alloc(width, height) + } else { + // Split holizontally + 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 n.child0.alloc(width, height) + } + } + if n.child0 == nil || n.child1 == nil { + panic("not reached") + } + if node := n.child0.alloc(width, height); node != nil { + return node + } + if node := n.child1.alloc(width, height); node != nil { + return node + } + return nil +} + +func (p *Page) Alloc(width, height int) *Node { + if width <= 0 || height <= 0 { + panic("bsp: width and height must > 0") + } + if p.root == nil { + p.root = &Node{ + width: MaxSize, + height: MaxSize, + } + } + if width < minSize { + width = minSize + } + if height < minSize { + height = minSize + } + return p.root.alloc(width, height) +} + +func (p *Page) Free(node *Node) { + if node.child0 != nil || node.child1 != nil { + panic("bsp: 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("not reached") + } + if node.parent.child0.canFree() && node.parent.child1.canFree() { + node.parent.child0 = nil + node.parent.child1 = nil + p.Free(node.parent) + } +} diff --git a/internal/bsp/bsp_test.go b/internal/bsp/bsp_test.go new file mode 100644 index 000000000..f2ea75b6d --- /dev/null +++ b/internal/bsp/bsp_test.go @@ -0,0 +1,208 @@ +// Copyright 2018 The Ebiten Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bsp_test + +import ( + "testing" + + . "github.com/hajimehoshi/ebiten/internal/bsp" +) + +func TestBSP(t *testing.T) { + type Rect struct { + X int + Y int + Width int + Height int + } + + type Op struct { + Width int + Height int + FreeNodeID int + } + + cases := []struct { + In []Op + Out []*Rect + }{ + { + In: []Op{ + {100, 100, -1}, + {100, 100, -1}, + {100, 100, -1}, + {100, 100, -1}, + {100, 100, -1}, + {100, 100, -1}, + {0, 0, 1}, + {0, 0, 3}, + {0, 0, 5}, + {0, 0, 0}, + {0, 0, 2}, + {0, 0, 4}, + {MaxSize, MaxSize, -1}, + }, + Out: []*Rect{ + {0, 0, 100, 100}, + {0, 100, 100, 100}, + {0, 200, 100, 100}, + {0, 300, 100, 100}, + {0, 400, 100, 100}, + {0, 500, 100, 100}, + nil, + nil, + nil, + nil, + nil, + nil, + {0, 0, MaxSize, MaxSize}, + }, + }, + { + In: []Op{ + {100, 200, -1}, + {MaxSize, MaxSize, -1}, + {100, 200, -1}, + {50, 50, -1}, + {200, 200, -1}, + {MaxSize, MaxSize, -1}, + {500, 500, -1}, + {600, 600, -1}, + {100, 100, -1}, + {0, 0, 2}, + {100, 200, -1}, + }, + Out: []*Rect{ + {0, 0, 100, 200}, + nil, + {0, 200, 100, 200}, + {0, 400, 50, 50}, + {100, 0, 200, 200}, + nil, + {300, 0, 500, 500}, + nil, + {100, 200, 100, 100}, + nil, + {0, 200, 100, 200}, + }, + }, + { + In: []Op{ + {256, 256, -1}, + {256, 256, -1}, + {256, 256, -1}, + {256, 256, -1}, + + {256, 256, -1}, + {256, 256, -1}, + {256, 256, -1}, + {256, 256, -1}, + + {256, 256, -1}, + {256, 256, -1}, + {256, 256, -1}, + {256, 256, -1}, + + {256, 256, -1}, + {256, 256, -1}, + {256, 256, -1}, + {256, 256, -1}, + + {256, 256, -1}, + }, + Out: []*Rect{ + {0, 0, 256, 256}, + {0, 256, 256, 256}, + {0, 512, 256, 256}, + {0, 768, 256, 256}, + + {256, 0, 256, 256}, + {256, 256, 256, 256}, + {256, 512, 256, 256}, + {256, 768, 256, 256}, + + {512, 0, 256, 256}, + {512, 256, 256, 256}, + {512, 512, 256, 256}, + {512, 768, 256, 256}, + + {768, 0, 256, 256}, + {768, 256, 256, 256}, + {768, 512, 256, 256}, + {768, 768, 256, 256}, + + nil, + }, + }, + { + In: []Op{ + {300, 300, -1}, + {300, 300, -1}, + {300, 300, -1}, + {300, 300, -1}, + {300, 300, -1}, + {300, 300, -1}, + {300, 300, -1}, + {300, 300, -1}, + {300, 300, -1}, + {300, 300, -1}, + }, + Out: []*Rect{ + {0, 0, 300, 300}, + {0, 300, 300, 300}, + {0, 600, 300, 300}, + {300, 0, 300, 300}, + {300, 300, 300, 300}, + {300, 600, 300, 300}, + {600, 0, 300, 300}, + {600, 300, 300, 300}, + {600, 600, 300, 300}, + nil, + }, + }, + } + + for caseIndex, c := range cases { + p := &Page{} + nodes := []*Node{} + for _, in := range c.In { + if in.FreeNodeID == -1 { + n := p.Alloc(in.Width, in.Height) + nodes = append(nodes, n) + } else { + p.Free(nodes[in.FreeNodeID]) + nodes = append(nodes, nil) + } + } + for i, out := range c.Out { + if nodes[i] == nil { + if out != nil { + t.Errorf("(%d) nodes[%d]: should be nil but %v", caseIndex, i, out) + } + continue + } + x, y, width, height := nodes[i].Region() + got := Rect{x, y, width, height} + if out == nil { + t.Errorf("(%d) nodes[%d]: got: %v, want: %v", caseIndex, i, got, nil) + continue + } + want := *out + if got != want { + t.Errorf("(%d) nodes[%d]: got: %v, want: %v", caseIndex, i, got, want) + } + } + } +}