1 /*
2     Copyright © 2020, Inochi2D Project
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors: Luna Nielsen
6 */
7 module creator.atlas.packer;
8 import inmath.linalg;
9 import inmath.math;
10 import std.exception;
11 import std.format;
12 import std.algorithm.mutation : remove;
13 
14 /**
15     Check if a proto "rectangle" contains an other rectangle
16 */
17 private bool contains(vec4i a, vec4i b) {
18     return  a.x >= b.x && 
19             a.y >= b.y &&
20             a.x+a.z <= b.x+b.z &&
21             a.y+a.w <= b.y+b.w;
22 }
23 
24 /**
25     A bin
26 */
27 private struct Bin {
28 private:
29     vec2i size;
30     vec4i[] usedRectangles;
31     vec4i[] freeRectangles;
32 
33     vec4i scoreRect(vec2i size, out int score1, out int score2) {
34         vec4i newNode;
35 
36         // Find the best place to put the rectangle
37         score1 = int.max;
38         score2 = int.max;
39         newNode = findNewNodeFit(size, score1, score2);
40 
41         // reset score
42         if (newNode.w == 0) {
43             score1 = int.max;
44             score2 = int.max;
45         }
46         return newNode;
47     }
48 
49     vec4i scoreRect(vec2i size) {
50         vec4i newNode;
51 
52         // Find the best place to put the rectangle
53         int score1 = int.max;
54         int score2 = int.max;
55         newNode = findNewNodeFit(size, score1, score2);
56 
57         return newNode;
58     }
59 
60     void place(ref vec4i newNode) {
61 
62         // Rectangles to process
63         size_t rectanglesToProcess = freeRectangles.length;
64 
65         // Run through all rectangles
66         for(int i; i < rectanglesToProcess; ++i) {
67 
68             // Try splitting them up
69             if (splitFree(freeRectangles[i], newNode)) {
70                 freeRectangles.remove(i);
71                 --i;
72                 --rectanglesToProcess;
73             }
74         }
75 
76         prune();
77         usedRectangles ~= newNode;
78     }
79     
80     vec4i findNewNodeFit(vec2i size, int score1, int score2) {
81         vec4i bestNode = vec4i.init;
82 
83         int bestShortFit = int.max;
84         int bestLongFit = int.max;
85 
86         foreach(freeRect; freeRectangles) {
87             
88             // Try placing the rectangle in upright orientation
89             if (freeRect.z >= size.x && freeRect.w >= size.y) {
90                 int leftoverH = abs(freeRect.z - size.x);
91                 int leftoverV = abs(freeRect.w - size.y);
92                 int shortSideFit = min(leftoverH, leftoverV);
93                 int longSideFit = max(leftoverH, leftoverV);
94 
95                 if (shortSideFit < bestShortFit || (shortSideFit == bestShortFit && longSideFit < bestLongFit)) {
96                     bestNode.x = freeRect.x;
97                     bestNode.y = freeRect.y;
98                     bestNode.z = size.x;
99                     bestNode.w = size.y;
100                     bestShortFit = shortSideFit;
101                     bestLongFit = longSideFit;
102                 }
103             }
104         }
105 
106         return bestNode;
107     }
108 
109     bool splitFree(vec4i freeNode, ref vec4i usedNode) {
110         if (usedNode.x >= freeNode.x + freeNode.z || usedNode.x + usedNode.z <= freeNode.x ||
111             usedNode.y >= freeNode.y + freeNode.w || usedNode.y + usedNode.w <= freeNode.y) 
112             return false;
113 
114         // Vertical Splitting
115         if (usedNode.x < freeNode.x + freeNode.z && usedNode.x + usedNode.z > freeNode.x) {
116 
117             // New node at top of used
118             if (usedNode.y > freeNode.y && usedNode.y < freeNode.y + freeNode.w) {
119                 vec4i newNode = freeNode;
120                 newNode.w = usedNode.y - newNode.y;
121                 freeRectangles ~= newNode;
122             }
123 
124             // New node at bottom of used
125             if (usedNode.y + usedNode.w < freeNode.y + freeNode.w) {
126                 vec4i newNode = freeNode;
127                 newNode.y = usedNode.y + usedNode.w;
128                 newNode.w = freeNode.y + freeNode.w - (usedNode.y + usedNode.w);
129                 freeRectangles ~= newNode;
130             }
131         }
132 
133         // Horizontal Splitting
134         if (usedNode.y < freeNode.y + freeNode.w && usedNode.y + usedNode.w > freeNode.y) {
135 
136             // New node at left of used
137             if (usedNode.x > freeNode.x && usedNode.x < freeNode.x + freeNode.z) {
138                 vec4i newNode = freeNode;
139                 newNode.z = usedNode.x - newNode.x;
140                 freeRectangles ~= newNode;
141             }
142 
143             // New node at right of used
144             if (usedNode.x + usedNode.z < freeNode.x + freeNode.z) {
145                 vec4i newNode = freeNode;
146                 newNode.x = usedNode.x + usedNode.z;
147                 newNode.z = freeNode.x + freeNode.z - (usedNode.x + usedNode.z);
148                 freeRectangles ~= newNode;
149             }
150         }
151         return true;
152     }
153 
154     void prune() {
155         for(int i; i < freeRectangles.length; ++i) {
156             for(int j = i+1; j < freeRectangles.length; ++j) {
157 
158 
159                 if (freeRectangles[i].contains(freeRectangles[j])) {
160                     freeRectangles = freeRectangles.remove(i);
161                     --i;
162                     break;
163                 }
164 
165                 if (freeRectangles[j].contains(freeRectangles[i])) {
166                     freeRectangles = freeRectangles.remove(j);
167                     --j;
168                 }
169             }
170         }
171     }
172 
173 public:
174     this(vec2i size) {
175         this.size = size;
176         freeRectangles = [vec4i(0, 0, size.x, size.y)];
177     }
178 
179     void resize(vec2i size) {
180         // TODO: Actually resize
181     }
182 
183     /**
184         Inserts a new rectangle in to the bin
185     */
186     vec4i insert(vec2i size) {
187         int score1;
188         int score2;
189         vec4i newNode = scoreRect(size, score1, score2);
190 
191         // Place rectangle in to bin
192         place(newNode);
193         return newNode;
194     }
195 
196     /**
197         Removes the area from the packing
198     */
199     void remove(vec4i area) {
200         foreach(i, rect; usedRectangles) {
201             if (rect == area) {
202                 usedRectangles = usedRectangles.remove(i);
203                 break;
204             }
205         }
206         freeRectangles ~= area;
207     }
208 
209     /**
210         Clears the bin
211     */
212     void clear() {
213         freeRectangles = [vec4i(0, 0, size.x, size.y)];
214         usedRectangles = [];
215     }
216 
217     /**
218         Gets ratio of surface area used
219     */
220     float occupancy() {
221         ulong surfaceArea = 0;
222         foreach(rect; usedRectangles) {
223             surfaceArea += rect.z*rect.w;
224         }
225         return surfaceArea / (size.x*size.y);
226     }
227 
228     /**
229         Gets the size of the bin
230     */
231     vec2i getSize() {
232         return size;
233     }
234 }
235 
236 /**
237     The texture packer
238 */
239 class TexturePacker {
240 private:
241     Bin bin;
242 
243 public:
244 
245     /**
246         Max size of texture packer
247     */
248     this(vec2i textureSize = vec2i(1024, 1024)) {
249         bin = Bin(textureSize);
250     }
251 
252     /**
253         Packs a texture in to the bin
254 
255         Returns a vec4i(0, 0, 0, 0) on packing failure
256     */
257     vec4i packTexture(vec2i size) {
258         return bin.insert(size);
259     }
260 
261     /**
262         Remove an area from the texture packer
263     */
264     void remove(vec4i area) {
265         bin.remove(area);
266     }
267 
268     /**
269         Clear the texture packer
270     */
271     void clear() {
272         bin.clear();
273     }
274 }