1 /**
2 Internal. Operations on list of 2D boxes.
3 
4 Copyright: Guillaume Piolat 2015.
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 * Authors:   Guillaume Piolat
7 */
8 module dplug.gui.boxlist;
9 
10 import dplug.core.vec;
11 import dplug.core.sync;
12 import dplug.math.box;
13 
14 
15 /// Returns: Bounding boxes of all bounding boxes.
16 box2i boundingBox(box2i[] boxes) pure nothrow @nogc
17 {
18     if (boxes.length == 0)
19         return box2i(0, 0, 0, 0);
20     else
21     {
22         // Computes union of all boxes
23         box2i unionBox = boxes[0];
24         for(int i = 1; i < cast(int)boxes.length; ++i)
25             unionBox = unionBox.expand(boxes[i]);
26         return unionBox;
27     }
28 }
29 
30 
31 /// Make 4 boxes that are A without C (C is contained in A)
32 /// Some may be empty though since C touch at least one edge of A
33 
34 /// General case
35 /// +---------+               +---------+
36 /// |    A    |               |    D    |
37 /// |  +---+  |   After split +--+---+--+
38 /// |  | C |  |        =>     | E|   |F |   At least one of D, E, F or G is empty
39 /// |  +---+  |               +--+---+--+
40 /// |         |               |    G    |
41 /// +---------+               +---------+
42 void boxSubtraction(in box2i A, in box2i C, out box2i D, out box2i E, out box2i F, out box2i G) pure nothrow @nogc
43 {
44     D = box2i(A.min.x, A.min.y, A.max.x, C.min.y);
45     E = box2i(A.min.x, C.min.y, C.min.x, C.max.y);
46     F = box2i(C.max.x, C.min.y, A.max.x, C.max.y);
47     G = box2i(A.min.x, C.max.y, A.max.x, A.max.y);
48 }
49 
50 // Change the list of boxes so that the coverage is the same but none overlaps
51 // Every box pushed in filtered are non-intersecting.
52 // This may modify boxes in input.
53 // FUTURE: something better than O(n^2)
54 void removeOverlappingAreas(ref Vec!box2i boxes, ref Vec!box2i filtered) nothrow @nogc
55 {
56     for(int i = 0; i < cast(int)(boxes.length); ++i)
57     {
58         box2i A = boxes[i];
59 
60         assert(A.isSorted());
61 
62         // empty boxes aren't kept
63         if (A.volume() <= 0)
64             continue;
65 
66         bool foundIntersection = false;
67 
68         // test A against all other rectangles, if it pass, it is pushed
69         for(int j = i + 1; j < cast(int)(boxes.length); ++j)
70         {
71             box2i B = boxes[j];
72 
73             box2i C = A.intersection(B);
74             bool doesIntersect =  C.isSorted() && (!C.empty());
75 
76             if (doesIntersect)
77             {
78                 // case 1: A contains B, B is removed from the array, and no intersection considered
79                 if (A.contains(B))
80                 {
81                     // Remove that box since it has been dealt with
82                     boxes.removeAndReplaceByLastElement(j);
83                     j = j - 1;
84                     continue;
85                 }
86 
87                 foundIntersection = true; // A will not be pushed as is
88 
89                 if (B.contains(A))
90                 {
91                     break; // nothing from A is kept
92                 }
93                 else
94                 {
95                     // computes A without C
96                     box2i D, E, F, G;
97                     boxSubtraction(A, C, D, E, F, G);
98 
99                     if (!D.empty)
100                         boxes.pushBack(D);
101                     if (!E.empty)
102                         boxes.pushBack(E);
103                     if (!F.empty)
104                         boxes.pushBack(F);
105                     if (!G.empty)
106                         boxes.pushBack(G);
107 
108                     // no need to search for other intersection in A, since its parts have
109                     // been pushed
110                     break;
111                 }
112             }
113         }
114 
115         if (!foundIntersection)
116             filtered.pushBack(A);
117     }
118 }
119 
120 unittest
121 {
122     auto bl = makeVec!box2i();
123     bl.pushBack( box2i(0, 0, 4, 4) );
124     bl.pushBack( box2i(2, 2, 6, 6) );
125     bl.pushBack( box2i(1, 1, 2, 2) );
126 
127     import dplug.core.vec;
128 
129     auto ab = makeVec!box2i();
130 
131     removeOverlappingAreas(bl, ab);
132     assert(ab[] == [ box2i(2, 2, 6, 6), box2i(0, 0, 4, 2), box2i(0, 2, 2, 4) ] );
133 
134     assert(bl[].boundingBox() == box2i(0, 0, 6, 6));
135 }
136 
137 
138 // Split each boxes in smaller boxes.
139 void tileAreas(in box2i[] areas, int maxWidth, int maxHeight, ref Vec!box2i splitted) nothrow @nogc
140 {
141     foreach(area; areas)
142     {
143         assert(!area.empty);
144         int nWidth = (area.width + maxWidth - 1) / maxWidth;
145         int nHeight = (area.height + maxHeight - 1) / maxHeight;
146 
147         foreach (int j; 0..nHeight)
148         {
149             int y0 = maxHeight * j;
150             int y1 = y0 + maxHeight;
151             if (y1 > area.height)
152                 y1 = area.height;
153 
154             foreach (int i; 0..nWidth)
155             {
156                 int x0 = maxWidth * i;
157                 int x1 = x0 + maxWidth;
158                 if (x1 > area.width)
159                     x1 = area.width;
160 
161                 box2i b = box2i(x0, y0, x1, y1).translate(area.min);
162                 assert(area.contains(b));
163                 splitted.pushBack(b);
164             }
165         }
166     }
167 }
168 
169 /// For debug purpose.
170 /// Returns: true if none of the boxes overlap.
171 bool haveNoOverlap(in box2i[] areas) nothrow @nogc
172 {
173     int N = cast(int)areas.length;
174 
175     // check every pair of boxes for overlap, inneficient
176     for (int i = 0; i < N; ++i)
177     {
178         assert(areas[i].isSorted());
179         for (int j = i + 1; j < N; ++j)
180         {
181             if (areas[i].intersects(areas[j]))
182                 return false;
183         }
184     }
185     return true;
186 }
187 
188 unittest
189 {
190     assert(haveNoOverlap([ box2i( 0, 0, 1, 1), box2i(1, 1, 2, 2) ]));
191     assert(!haveNoOverlap([ box2i( 0, 0, 1, 1), box2i(0, 0, 2, 1) ]));
192 }
193 
194 
195 DirtyRectList makeDirtyRectList() nothrow @nogc
196 {
197     return DirtyRectList(4);
198 }
199 
200 struct DirtyRectList
201 {
202 public:
203     nothrow @nogc:
204 
205     this(int dummy) 
206     {
207         _dirtyRectMutex = makeMutex();
208         _dirtyRects = makeVec!box2i(0);
209     }
210 
211     @disable this(this);
212 
213     bool isEmpty() nothrow @nogc
214     {
215         _dirtyRectMutex.lock();
216         bool result = _dirtyRects.length == 0;
217         _dirtyRectMutex.unlock();
218         return result;
219     }
220 
221     /// Returns: Array of rectangles in the list, remove them from the list.
222     /// Needed to avoid races in repainting.
223     void pullAllRectangles(ref Vec!box2i result) nothrow @nogc
224     {
225         _dirtyRectMutex.lock();
226 
227         foreach(rect; _dirtyRects[])
228             result.pushBack(rect);
229 
230         _dirtyRects.clearContents();
231 
232         _dirtyRectMutex.unlock();
233     }
234 
235     /// Add a rect while keeping the no overlap invariant
236     void addRect(box2i rect) nothrow @nogc
237     {
238         assert(rect.isSorted);
239 
240         if (!rect.empty)
241         {
242             _dirtyRectMutex.lock();
243             scope(exit) _dirtyRectMutex.unlock();
244 
245             bool processed = false;
246 
247             for (int i = 0; i < _dirtyRects.length; ++i)
248             {
249                 box2i other = _dirtyRects[i];
250                 if (other.contains(rect))
251                 {
252                     // If the rectangle candidate is inside an element of the list, discard it.
253                     processed = true;
254                     break;
255                 }
256                 else if (rect.contains(other)) // remove rect that it contains
257                 {
258                     // If the rectangle candidate contains an element of the list, this element need to go.
259                     _dirtyRects[i] = _dirtyRects.popBack();
260                     i--;
261                 }
262                 else
263                 {
264                     box2i common = other.intersection(rect);
265                     if (!common.empty())
266                     {
267                         // compute other without common
268                         box2i D, E, F, G;
269                         boxSubtraction(other, common, D, E, F, G);
270 
271                         // remove other from list
272                         _dirtyRects[i] = _dirtyRects.popBack();
273                         i--;
274 
275                         // push the sub parts at the end of the list
276                         // this is guaranteed to be non-overlapping since the list was non-overlapping before
277                         if (!D.empty) _dirtyRects.pushBack(D);
278                         if (!E.empty) _dirtyRects.pushBack(E);
279                         if (!F.empty) _dirtyRects.pushBack(F);
280                         if (!G.empty) _dirtyRects.pushBack(G);
281                     }
282                     // else no intersection problem, the candidate rectangle will be pushed normally in the list
283                 }
284 
285             }
286 
287             if (!processed)
288                 _dirtyRects.pushBack(rect);
289 
290             // Quadratic test, disabled
291             // assert(haveNoOverlap(_dirtyRects[]));
292         }
293     }
294 
295 private:
296     /// The possibly overlapping areas that need updating.
297     Vec!box2i _dirtyRects;
298 
299     /// This is protected by a mutex, because it is sometimes updated from the host.
300     /// Note: we cannot remove this mutex, as host parameter change call setDirtyWhole directly.
301     /// TODO: we want to remove this lock, the host thread may avoid doing it directly.
302     UncheckedMutex _dirtyRectMutex;
303 }
304