1 /++
2  + Authors: Avaxar <avaxar@nekkl.org>
3  + Copyright: Copyright © 2023, Avaxar
4  + License: $(LINK2 https://mit-license.org, MIT License)
5  +/
6 
7 module dsdl2.rect;
8 @safe:
9 
10 import bindbc.sdl;
11 import dsdl2.sdl;
12 import dsdl2.frect;
13 
14 import std.conv : to;
15 import std.format : format;
16 import std.typecons : Nullable, nullable;
17 
18 /++
19  + D struct that wraps `SDL_Point` containing 2D integer coordinate pair
20  +
21  + `dsdl2.Point` stores signed `int`eger `x` and `y` coordinate points. This wrapper also implements vector-like
22  + operator overloading.
23  +/
24 struct Point {
25     SDL_Point sdlPoint; /// Internal `SDL_Point` struct
26 
27     this() @disable;
28 
29     /++
30      + Constructs a `dsdl2.Point` from a vanilla `SDL_Point` from bindbc-sdl
31      +
32      + Params:
33      +   sdlPoint = the `dsdl2.Point` struct
34      +/
35     this(SDL_Point sdlPoint) {
36         this.sdlPoint = sdlPoint;
37     }
38 
39     /++
40      + Constructs a `dsdl2.Point` by feeding in an `x` and `y` pair
41      +
42      + Params:
43      +   x = x coordinate point
44      +   y = y coordinate point
45      +/
46     this(int x, int y) {
47         this.sdlPoint.x = x;
48         this.sdlPoint.y = y;
49     }
50 
51     /++
52      + Constructs a `dsdl2.Point` by feeding in an array of `x` and `y`
53      +
54      + Params:
55      +   xy = x and y coordinate point array
56      +/
57     this(int[2] xy) {
58         this.sdlPoint.x = xy[0];
59         this.sdlPoint.y = xy[1];
60     }
61 
62     static if (sdlSupport >= SDLSupport.v2_0_10) {
63         /++
64          + Constructs a `dsdl2.Point` from a `dsdl2.FPoint` (from SDL 2.0.10)
65          +
66          + Params:
67          +   fpoint = `dsdl2.FPoint` whose attributes are to be copied
68          +/
69         this(FPoint fpoint) {
70             this.sdlPoint.x = fpoint.x.to!int;
71             this.sdlPoint.y = fpoint.y.to!int;
72         }
73     }
74 
75     /++
76      + Unary element-wise operation overload template
77      +/
78     Point opUnary(string op)() const {
79         return Point(mixin(op ~ "this.x"), mixin(op ~ "this.y"));
80     }
81 
82     /++
83      + Binary element-wise operation overload template
84      +/
85     Point opBinary(string op)(const Point other) const {
86         return Point(mixin("this.x" ~ op ~ "other.x"), mixin("this.y" ~ op ~ "other.y"));
87     }
88 
89     /++
90      + Binary operation overload template with scalars
91      +/
92     Point opBinary(string op)(int scalar) const if (op == "*" || op == "/") {
93         return Point(mixin("this.x" ~ op ~ "scalar"), mixin("this.y" ~ op ~ "scalar"));
94     }
95 
96     /++
97      + Element-wise operator assignment overload
98      +/
99     ref inout(Point) opOpAssign(string op)(const Point other) return inout {
100         mixin("this.x" ~ op ~ "=other.x");
101         mixin("this.y" ~ op ~ "=other.y");
102         return this;
103     }
104 
105     /++
106      + Operator assignment overload with scalars
107      +/
108     ref inout(Point) opOpAssign(string op)(const int scalar) return inout
109     if (op == "*" || op == "/") {
110         mixin("this.x" ~ op ~ "=scalar");
111         mixin("this.y" ~ op ~ "=scalar");
112         return this;
113     }
114 
115     /++
116      + Formats the `dsdl2.Point` into its construction representation: `"dsdl2.Point(<x>, <y>)"`
117      +
118      + Returns: the formatted `string`
119      +/
120     string toString() const {
121         return "dsdl2.Point(%d, %d)".format(this.x, this.y);
122     }
123 
124     /++
125      + Proxy to the X value of the `dsdl2.Point`
126      +
127      + Returns: X value of the `dsdl2.Point`
128      +/
129     ref inout(int) x() return inout @property {
130         return this.sdlPoint.x;
131     }
132 
133     /++
134      + Proxy to the Y value of the `dsdl2.Point`
135      +
136      + Returns: Y value of the `dsdl2.Point`
137      +/
138     ref inout(int) y() return inout @property {
139         return this.sdlPoint.y;
140     }
141 
142     /++
143      + Static array proxy of the `dsdl2.Point`
144      +
145      + Returns: array of `x` and `y`
146      +/
147     ref inout(int[2]) array() return inout @property @trusted {
148         return *cast(inout(int[2]*))&this.sdlPoint;
149     }
150 }
151 ///
152 unittest {
153     auto a = dsdl2.Point(1, 2);
154     auto b = a + a;
155     assert(b == dsdl2.Point(2, 4));
156 
157     auto c = a * 2;
158     assert(b == c);
159 }
160 
161 /++
162  + D struct that wraps `SDL_Rect` representing a rectangle of integer 2D coordinate and dimension
163  +
164  + `dsdl2.Rect` stores signed `int`eger `x` and `y` coordinate points, as well as `w`idth and `h`eight which
165  + specifies the rectangle's dimension. `x` and `y` symbolize the top-left coordinate of the rectangle, and
166  + the `w`idth and `h`eight extend to the positive plane of both axes.
167  +/
168 struct Rect {
169     SDL_Rect sdlRect; /// Internal `SDL_Rect` struct
170 
171     this() @disable;
172 
173     /++
174      + Constructs a `dsdl2.Rect` from a vanilla `SDL_Rect` from bindbc-sdl
175      +
176      + Params:
177      +   sdlRect = the `SDL_Rect` struct
178      +/
179     this(SDL_Rect sdlRect) {
180         this.sdlRect = sdlRect;
181     }
182 
183     /++
184      + Constructs a `dsdl2.Rect` by feeding in the `x`, `y`, `width`, and `height` of the rectangle
185      +
186      + Params:
187      +   x = top-left x coordinate point of the rectangle
188      +   y = top-left y coordinate point of the rectangle
189      +   width = rectangle width
190      +   height = rectangle height
191      +/
192     this(int x, int y, int width, int height) {
193         this.sdlRect.x = x;
194         this.sdlRect.y = y;
195         this.sdlRect.w = width;
196         this.sdlRect.h = height;
197     }
198 
199     /++
200      + Constructs a `dsdl2.Rect` by feeding in a `dsdl2.Point` as the `x` and `y`, then `width` and `height` of the
201      + rectangle
202      +
203      + Params:
204      +   point = top-left point of the rectangle
205      +   width = rectangle width
206      +   height = rectangle height
207      +/
208     this(Point point, int width, int height) {
209         this.sdlRect.x = point.x;
210         this.sdlRect.y = point.y;
211         this.sdlRect.w = width;
212         this.sdlRect.h = height;
213     }
214 
215     static if (sdlSupport >= SDLSupport.v2_0_10) {
216         /++
217          + Constructs a `dsdl2.Rect` from a `dsdl2.FRect` (from SDL 2.0.10)
218          +
219          + Params:
220          +   frect = `dsdl2.FRect` whose attributes are to be copied
221          +/
222         this(FRect frect) {
223             this.sdlRect.x = frect.x.to!int;
224             this.sdlRect.y = frect.y.to!int;
225             this.sdlRect.w = frect.width.to!int;
226             this.sdlRect.h = frect.height.to!int;
227         }
228     }
229 
230     /++
231      + Binary operation overload template to move rectangle's position by an `offset` as a `dsdl2.Point`
232      +/
233     Rect opBinary(string op)(const Point offset) const if (op == "+" || op == "-") {
234         return Rect(Point(mixin("this.x" ~ op ~ "offset.x"), mixin("this.y" ~ op ~ "offset.y")),
235             this.w, this.h);
236     }
237 
238     /++
239      + Operator assignment overload template to move rectangle's position in-place by an `offset` as a `dsdl2.Point`
240      +/
241     ref inout(Point) opOpAssign(string op)(const Point offset) return inout
242     if (op == "+" || op == "-") {
243         mixin("this.x" ~ op ~ "=offset.x");
244         mixin("this.y" ~ op ~ "=offset.y");
245         return this;
246     }
247 
248     /++
249      + Formats the `dsdl2.Rect` into its construction representation: `"dsdl2.Rect(<x>, <y>, <w>, <h>)"`
250      +
251      + Returns: the formatted `string`
252      +/
253     string toString() const {
254         return "dsdl2.Rect(%d, %d, %d, %d)".format(this.x, this.y, this.width, this.height);
255     }
256 
257     /++
258      + Proxy to the X value of the `dsdl2.Rect`
259      +
260      + Returns: X value of the `dsdl2.Rect`
261      +/
262     ref inout(int) x() return inout @property {
263         return this.sdlRect.x;
264     }
265 
266     /++
267      + Proxy to the Y value of the `dsdl2.Rect`
268      +
269      + Returns: Y value of the `dsdl2.Rect`
270      +/
271     ref inout(int) y() return inout @property {
272         return this.sdlRect.y;
273     }
274 
275     /++
276      + Proxy to the `dsdl2.Point` containing the `x` and `y` value of the `dsdl2.Rect`
277      +
278      + Returns: reference to the `dsdl2.Point` structure
279      +/
280     ref inout(Point) point() return inout @property @trusted {
281         return *cast(inout(Point*))&this.sdlRect.x;
282     }
283 
284     /++
285      + Proxy to the width of the `dsdl2.Rect`
286      +
287      + Returns: width of the `dsdl2.Rect`
288      +/
289     ref inout(int) width() return inout @property {
290         return this.sdlRect.w;
291     }
292 
293     /++
294      + Proxy to the height of the `dsdl2.Rect`
295      +
296      + Returns: height of the `dsdl2.Rect`
297      +/
298     ref inout(int) height() return inout @property {
299         return this.sdlRect.h;
300     }
301 
302     /++
303      + Proxy to the size array containing the `width` and `height` of the `dsdl2.Rect`
304      +
305      + Returns: reference to the static `int[2]` array
306      +/
307     ref inout(int[2]) size() return inout @property @trusted {
308         return *cast(inout(int[2]*))&this.sdlRect.w;
309     }
310 
311     /++
312      + Wraps `SDL_RectEmpty` which checks if the `dsdl2.Rect` is an empty rectangle
313      +
314      + Returns: `true` if it is empty, otherwise `false`
315      +/
316     bool empty() const @trusted {
317         return SDL_RectEmpty(&this.sdlRect);
318     }
319 
320     /++
321      + Wraps `SDL_PointInRect` which sees whether the coordinate of a `dsdl2.Point` is inside the `dsdl2.Rect`
322      +
323      + Params:
324      +   point = the `dsdl2.Point` to check its collision of with the `dsdl2.Rect` instance
325      + Returns: `true` if it is within, otherwise `false`
326      +/
327     bool pointInRect(Point point) const @trusted {
328         return SDL_PointInRect(&point.sdlPoint, &this.sdlRect);
329     }
330 
331     /++
332      + Wraps `SDL_HasIntersection` which sees whether two `dsdl2.Rect`s intersect each other
333      +
334      + Params:
335      +   other = other `dsdl2.Rect` to check its intersection of with the `dsdl2.Rect`
336      + Returns: `true` if both have intersection with each other, otherwise `false`
337      +/
338     bool hasIntersection(Rect other) const @trusted {
339         return SDL_HasIntersection(&this.sdlRect, &other.sdlRect) == SDL_TRUE;
340     }
341 
342     /++
343      + Wraps `SDL_IntersectRectAndLine` which sees whether a line intersects with the `dsdl2.Rect`
344      +
345      + Params:
346      +   line = set of two `dsdl2.Point`s denoting the start and end coordinates of the line to check its intersection
347      +          of with the `dsdl2.Rect`
348      + Returns: `true` if it intersects, otherwise `false`
349      +/
350     bool hasLineIntersection(Point[2] line) const @trusted {
351         return SDL_IntersectRectAndLine(&this.sdlRect, &line[0].sdlPoint.x, &line[0].sdlPoint.y,
352             &line[1].sdlPoint.x, &line[1].sdlPoint.y) == SDL_TRUE;
353     }
354 
355     /++
356      + Wraps `SDL_IntersectRect` which attempts to get the rectangle of intersection between two `dsdl2.Rect`s
357      +
358      + Params:
359      +   other = other `dsdl2.Rect` with which the `dsdl2.Rect` is intersected
360      + Returns: non-null `Nullable!Rect` instance if intersection is present, otherwise a null one
361      +/
362     Nullable!Rect intersectRect(Rect other) const @trusted {
363         Rect intersection = void;
364         if (SDL_IntersectRect(&this.sdlRect, &other.sdlRect, &intersection.sdlRect) == SDL_TRUE) {
365             return intersection.nullable;
366         }
367         else {
368             return Nullable!Rect.init;
369         }
370     }
371 
372     /++
373      + Wraps `SDL_IntersectRectAndLine` which attempts to clip a line segment in the boundaries of the `dsdl2.Rect`
374      +
375      + Params:
376      +   line = set of two `dsdl2.Point`s denoting the start and end coordinates of the line to clip from
377      +          its intersection with the `dsdl2.Rect`
378      + Returns: non-null `Nullable!(Point[2])` as the clipped line if there is an intersection, otherwise a null one
379      +/
380     Nullable!(Point[2]) intersectLine(Point[2] line) const @trusted {
381         if (SDL_IntersectRectAndLine(&this.sdlRect, &line[0].sdlPoint.x, &line[0].sdlPoint.y,
382                 &line[1].sdlPoint.x, &line[1].sdlPoint.y) == SDL_TRUE) {
383             Point[2] intersection = [line[0], line[1]];
384             return intersection.nullable;
385         }
386         else {
387             return Nullable!(Point[2]).init;
388         }
389     }
390 
391     /++
392      + Wraps `SDL_UnionRect` which creates a `dsdl2.Rect` of the minimum size to enclose two given `dsdl2.Rect`s
393      +
394      + Params:
395      +   other = other `dsdl2.Rect` to unify with the `dsdl2.Rect`
396      + Returns: `dsdl2.Rect` of the minimum size to enclose the `dsdl2.Rect` and `other`
397      +/
398     Rect unify(Rect other) const @trusted {
399         Rect union_ = void;
400         SDL_UnionRect(&this.sdlRect, &other.sdlRect, &union_.sdlRect);
401         return union_;
402     }
403 }
404 ///
405 unittest {
406     auto rect1 = dsdl2.Rect(-2, -2, 3, 3);
407     auto rect2 = dsdl2.Rect(-1, -1, 3, 3);
408 
409     assert(rect1.hasIntersection(rect2));
410     assert(rect1.intersectRect(rect2).get == dsdl2.Rect(-1, -1, 2, 2));
411 }