File: Packer.py

package info (click to toggle)
0ad 0.0.17-1
  • links: PTS, VCS
  • area: main
  • in suites: jessie, jessie-kfreebsd
  • size: 51,248 kB
  • ctags: 46,933
  • sloc: cpp: 223,208; ansic: 31,240; python: 16,343; perl: 4,083; sh: 1,011; makefile: 915; xml: 733; java: 621; ruby: 229; erlang: 53; sql: 40
file content (272 lines) | stat: -rw-r--r-- 11,904 bytes parent folder | download | duplicates (7)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# Adapted from:
#   http://enichan.darksiren.net/wordpress/?p=49
# which was adapted from some version of
#   https://devel.nuclex.org/framework/browser/game/Nuclex.Game/trunk/Source/Packing/CygonRectanglePacker.cs
# which has the following license:
#
#   Copyright (C) 2002-2009 Nuclex Development Labs
#
#   This library is free software; you can redistribute it and/or
#   modify it under the terms of the IBM Common Public License as
#   published by the IBM Corporation; either version 1.0 of the
#   License, or (at your option) any later version.
#
#   This library is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
#   IBM Common Public License for more details.

from bisect import bisect_left

class OutOfSpaceError(Exception): pass

class Point(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
 
    def __cmp__(self, other):
        """Compares the starting position of height slices"""
        return self.x - other.x

class RectanglePacker(object):
    """Base class for rectangle packing algorithms
 
    By uniting all rectangle packers under this common base class, you can
    easily switch between different algorithms to find the most efficient or
    performant one for a given job.
 
    An almost exhaustive list of packing algorithms can be found here:
    http://www.csc.liv.ac.uk/~epa/surveyhtml.html"""
 
    def __init__(self, packingAreaWidth, packingAreaHeight):
        """Initializes a new rectangle packer
 
        packingAreaWidth: Maximum width of the packing area
        packingAreaHeight: Maximum height of the packing area"""
        self.packingAreaWidth = packingAreaWidth
        self.packingAreaHeight = packingAreaHeight
 
    def Pack(self, rectangleWidth, rectangleHeight):
        """Allocates space for a rectangle in the packing area
 
        rectangleWidth: Width of the rectangle to allocate
        rectangleHeight: Height of the rectangle to allocate
 
        Returns the location at which the rectangle has been placed"""
        point = self.TryPack(rectangleWidth, rectangleHeight)
 
        if not point:
            raise OutOfSpaceError("Rectangle does not fit in packing area")
 
        return point
 
    def TryPack(self, rectangleWidth, rectangleHeight):
        """Tries to allocate space for a rectangle in the packing area
 
        rectangleWidth: Width of the rectangle to allocate
        rectangleHeight: Height of the rectangle to allocate
 
        Returns a Point instance if space for the rectangle could be allocated
        be found, otherwise returns None"""
        raise NotImplementedError

class DumbRectanglePacker(RectanglePacker):
    def __init__(self, packingAreaWidth, packingAreaHeight):
        RectanglePacker.__init__(self, packingAreaWidth, packingAreaHeight)
        self.x = 0
        self.y = 0
        self.rowh = 0

    def TryPack(self, rectangleWidth, rectangleHeight):
        if self.x + rectangleWidth >= self.packingAreaWidth:
            self.x = 0
            self.y += self.rowh
            self.rowh = 0
            if self.y + rectangleHeight >= self.packingAreaHeight:
                return None

        r = Point(self.x, self.y)
        self.x += rectangleWidth
        self.rowh = max(self.rowh, rectangleHeight)
        return r

class CygonRectanglePacker(RectanglePacker):
    """
    Packer using a custom algorithm by Markus 'Cygon' Ewald
 
    Algorithm conceived by Markus Ewald (cygon at nuclex dot org), though
    I'm quite sure I'm not the first one to come up with it :)
 
    The algorithm always places rectangles as low as possible in the packing
    area. So, for any new rectangle that is to be added, the packer has to
    determine the X coordinate at which the rectangle can have the lowest
    overall height without intersecting any other rectangles.
 
    To quickly discover these locations, the packer uses a sophisticated
    data structure that stores the upper silhouette of the packing area. When
    a new rectangle needs to be added, only the silouette edges need to be
    analyzed to find the position where the rectangle would achieve the lowest"""
 
    def __init__(self, packingAreaWidth, packingAreaHeight):
        """Initializes a new rectangle packer
 
        packingAreaWidth: Maximum width of the packing area
        packingAreaHeight: Maximum height of the packing area"""
        RectanglePacker.__init__(self, packingAreaWidth, packingAreaHeight)
 
        # Stores the height silhouette of the rectangles
        self.heightSlices = []
 
        # At the beginning, the packing area is a single slice of height 0
        self.heightSlices.append(Point(0,0))
 
    def TryPack(self, rectangleWidth, rectangleHeight):
        """Tries to allocate space for a rectangle in the packing area
 
        rectangleWidth: Width of the rectangle to allocate
        rectangleHeight: Height of the rectangle to allocate
 
        Returns a Point instance if space for the rectangle could be allocated
        be found, otherwise returns None"""
        placement = None
 
        # If the rectangle is larger than the packing area in any dimension,
        # it will never fit!
        if rectangleWidth > self.packingAreaWidth or rectangleHeight > \
        self.packingAreaHeight:
            return None
 
        # Determine the placement for the new rectangle
        placement = self.tryFindBestPlacement(rectangleWidth, rectangleHeight)
 
        # If a place for the rectangle could be found, update the height slice
        # table to mark the region of the rectangle as being taken.
        if placement:
            self.integrateRectangle(placement.x, rectangleWidth, placement.y \
            + rectangleHeight)
 
        return placement
 
    def tryFindBestPlacement(self, rectangleWidth, rectangleHeight):
        """Finds the best position for a rectangle of the given dimensions
 
        rectangleWidth: Width of the rectangle to find a position for
        rectangleHeight: Height of the rectangle to find a position for
 
        Returns a Point instance if a valid placement for the rectangle could
        be found, otherwise returns None"""
        # Slice index, vertical position and score of the best placement we
        # could find
        bestSliceIndex = -1 # Slice index where the best placement was found
        bestSliceY = 0 # Y position of the best placement found
        # lower == better!
        bestScore = self.packingAreaHeight 

        # This is the counter for the currently checked position. The search
        # works by skipping from slice to slice, determining the suitability
        # of the location for the placement of the rectangle.
        leftSliceIndex = 0
 
        # Determine the slice in which the right end of the rectangle is located
        rightSliceIndex = bisect_left(self.heightSlices, Point(rectangleWidth, 0))
 
        while rightSliceIndex <= len(self.heightSlices):
            # Determine the highest slice within the slices covered by the
            # rectangle at its current placement. We cannot put the rectangle
            # any lower than this without overlapping the other rectangles.
            highest = self.heightSlices[leftSliceIndex].y
            for index in xrange(leftSliceIndex + 1, rightSliceIndex):
                if self.heightSlices[index].y > highest:
                    highest = self.heightSlices[index].y
 
            # Only process this position if it doesn't leave the packing area
            if highest + rectangleHeight < self.packingAreaHeight:
                score = highest
 
                if score < bestScore:
                    bestSliceIndex = leftSliceIndex
                    bestSliceY = highest
                    bestScore = score
 
            # Advance the starting slice to the next slice start
            leftSliceIndex += 1
            if leftSliceIndex >= len(self.heightSlices):
                break
 
            # Advance the ending slice until we're on the proper slice again,
            # given the new starting position of the rectangle.
            rightRectangleEnd = self.heightSlices[leftSliceIndex].x + rectangleWidth
            while rightSliceIndex <= len(self.heightSlices):
                if rightSliceIndex == len(self.heightSlices):
                    rightSliceStart = self.packingAreaWidth
                else:
                    rightSliceStart = self.heightSlices[rightSliceIndex].x
 
                # Is this the slice we're looking for?
                if rightSliceStart > rightRectangleEnd:
                    break
 
                rightSliceIndex += 1
 
            # If we crossed the end of the slice array, the rectangle's right
            # end has left the packing area, and thus, our search ends.
            if rightSliceIndex > len(self.heightSlices):
                break
 
        # Return the best placement we found for this rectangle. If the
        # rectangle didn't fit anywhere, the slice index will still have its
        # initialization value of -1 and we can report that no placement
        # could be found.
        if bestSliceIndex == -1:
            return None
        else:
            return Point(self.heightSlices[bestSliceIndex].x, bestSliceY)
 
    def integrateRectangle(self, left, width, bottom):
        """Integrates a new rectangle into the height slice table
 
        left: Position of the rectangle's left side
        width: Width of the rectangle
        bottom: Position of the rectangle's lower side"""
        # Find the first slice that is touched by the rectangle
        startSlice = bisect_left(self.heightSlices, Point(left, 0))
 
        # We scored a direct hit, so we can replace the slice we have hit
        firstSliceOriginalHeight = self.heightSlices[startSlice].y
        self.heightSlices[startSlice] = Point(left, bottom)
 
        right = left + width
        startSlice += 1
 
        # Special case, the rectangle started on the last slice, so we cannot
        # use the start slice + 1 for the binary search and the possibly
        # already modified start slice height now only remains in our temporary
        # firstSliceOriginalHeight variable
        if startSlice >= len(self.heightSlices):
            # If the slice ends within the last slice (usual case, unless it
            # has the exact same width the packing area has), add another slice
            # to return to the original height at the end of the rectangle.
            if right < self.packingAreaWidth:
                self.heightSlices.append(Point(right, firstSliceOriginalHeight))
        else: # The rectangle doesn't start on the last slice
            endSlice = bisect_left(self.heightSlices, Point(right,0), \
            startSlice, len(self.heightSlices))
 
            # Another direct hit on the final slice's end?
            if endSlice < len(self.heightSlices) and not (Point(right, 0) < self.heightSlices[endSlice]):
                del self.heightSlices[startSlice:endSlice]
            else: # No direct hit, rectangle ends inside another slice
                # Find out to which height we need to return at the right end of
                # the rectangle
                if endSlice == startSlice:
                    returnHeight = firstSliceOriginalHeight
                else:
                    returnHeight = self.heightSlices[endSlice - 1].y
 
                # Remove all slices covered by the rectangle and begin a new
                # slice at its end to return back to the height of the slice on
                # which the rectangle ends.
                del self.heightSlices[startSlice:endSlice]
                if right < self.packingAreaWidth:
                    self.heightSlices.insert(startSlice, Point(right, returnHeight))