1 module dvec.vector;
2 
3 import std.traits : isNumeric, isFloatingPoint;
4 
5 /** 
6  * Generic struct that represents a vector holding `size` elements of type `T`.
7  * A vector must contain at least 1 element, and has no upper-bound on the size
8  * beyond restrictions imposed by the system.
9  */
10 struct Vec(T, size_t size) if (isNumeric!T && size > 0) {
11     /** 
12      * A static contstant that can be used to get the size of the vector.
13      */
14     public static const size_t SIZE = size;
15 
16     /** 
17      * The internal static array storing the elements of this vector.
18      */
19     public T[size] data;
20 
21     /** 
22      * Constructs a vector from an array of elements.
23      * Params:
24      *   elements = The elements to put in the vector.
25      */
26     public this(T[size] elements) {
27         static foreach (i; 0 .. size) data[i] = elements[i];
28     }
29 
30     /** 
31      * Constructs a vector from a variadic list of elements.
32      * Params:
33      *   elements = The elements to put in the vector.
34      */
35     public this(T[] elements...) {
36         if (elements.length != size) assert(false, "Invalid number of elements provided to Vec constructor.");
37         static foreach (i; 0 .. size) data[i] = elements[i];
38     }
39 
40     /** 
41      * Constructs a vector where all elements have the given value.
42      * Params:
43      *   value = The value to assign to all elements in the vector.
44      */
45     public this(T value) {
46         static foreach (i; 0 .. size) data[i] = value;
47     }
48 
49     /** 
50      * Constructs a vector as a copy of the other.
51      * Params:
52      *   other = The vector to copy.
53      */
54     public this(Vec!(T, size) other) {
55         this(other.data);
56     }
57 
58     /** 
59      * Constructs a vector containing all 0's.
60      * Returns: An vector containing all 0's.
61      */
62     public static Vec!(T, size) empty() {
63         Vec!(T, size) v;
64         static foreach (i; 0 .. size) v[i] = 0;
65         return v;
66     }
67 
68     public T opIndex(size_t i) {
69         return data[i];
70     }
71 
72     public void opIndexAssign(T value, size_t i) {
73         data[i] = value;
74     }
75 
76     public void add(V)(Vec!(V, size) other) if (isNumeric!V) {
77         static foreach (i; 0 .. size) data[i] += other[i];
78     }
79 
80     public void sub(V)(Vec!(V, size) other) if (isNumeric!V) {
81         static foreach (i; 0 .. size) data[i] -= other[i];
82     }
83 
84     alias subtract = sub;
85 
86     public void mul(V)(V factor) if (isNumeric!V) {
87         static foreach (i; 0 .. size) data[i] *= factor;
88     }
89 
90     alias multiply = mul;
91 
92     public void div(V)(V factor) if (isNumeric!V) {
93         static foreach (i; 0 .. size) data[i] /= factor;
94     }
95 
96     alias divide = div;
97 
98     public double mag() {
99         import std.math : sqrt;
100         double sum = 0;
101         static foreach (i; 0 .. size) sum += data[i] * data[i];
102         return sqrt(sum);
103     }
104 
105     alias magnitude = mag;
106     alias length = mag;
107     alias len = mag;
108 
109     public T dot(Vec!(T, size) other) {
110         T sum = 0;
111         static foreach (i; 0 .. size) sum += data[i] * other[i];
112         return sum;
113     }
114 
115     alias dotProduct = dot;
116 
117     // TODO: Make this @nogc compatible!
118     public string toString() const {
119         import std.conv : to;
120         string s = "[";
121         static foreach (i; 0 .. size - 1) {
122             s ~= data[i].to!string;
123             s ~= ", ";
124         }
125         s ~= data[size - 1].to!string;
126         s ~= "]";
127         return s;
128     }
129 
130     static if (isFloatingPoint!T) {
131         public void norm() {
132             const double mag = mag();
133             static foreach (i; 0 .. size) {
134                 data[i] /= mag;
135             }
136         }
137 
138         alias normalize = norm;
139     }
140 
141     static if (isFloatingPoint!T && size == 2) {
142         public void toPolar() {
143             import std.math : atan2;
144             T radius = mag();
145             T angle = atan2(data[1], data[0]);
146             data[0] = radius;
147             data[1] = angle;
148         }
149 
150         public void toCartesian() {
151             import std.math : cos, sin;
152             T x = data[0] * cos(data[1]);
153             T y = data[0] * sin(data[1]);
154             data[0] = x;
155             data[1] = y;
156         }
157     }
158 }
159 
160 // Aliases for common vector types.
161 alias Vec2f = Vec!(float, 2);
162 alias Vec3f = Vec!(float, 3);
163 alias Vec4f = Vec!(float, 4);
164 
165 alias Vec2d = Vec!(double, 2);
166 alias Vec3d = Vec!(double, 3);
167 alias Vec4d = Vec!(double, 4);
168 
169 alias Vec2i = Vec!(int, 2);
170 alias Vec3i = Vec!(int, 3);
171 alias Vec4i = Vec!(int, 4);
172 
173 unittest {
174     import std.stdio;
175     import std.math;
176 
177     void assertFP(double actual, double expected, double delta = 1e-06, string msg = "Assertion failed.") {
178         double lowBound = expected - delta;
179         double highBound = expected + delta;
180         assert(actual > lowBound && actual < highBound, msg);
181     }
182 
183     // Test constructors.
184     auto v1 = Vec2d([1.0, 2.0]);
185     assert(v1.data == [1.0, 2.0]);
186     auto v2 = Vec2d(1.0, 2.0);
187     assert(v2.data == [1.0, 2.0]);
188     auto v3 = Vec3f(3.14f);
189     assert(v3.data == [3.14f, 3.14f, 3.14f]);
190     auto v4 = Vec3f(v3);
191     assert(v4.data == v3.data);
192     auto v5 = Vec2i.empty();
193     assert(v5.data == [0, 0]);
194 
195     // Test basic methods.
196     v1.add(v2);
197     assert(v1.data == [2.0, 4.0]);
198     assert(v1[0] == 2.0);
199     assert(v1[1] == 4.0);
200     v1.sub(v2);
201     assert(v1.data == [1.0, 2.0]);
202     v1.mul(3.0);
203     assert(v1.data == [3.0, 6.0]);
204     v1.div(6.0);
205     assert(v1.data == [0.5, 1.0]);
206     assert(Vec2f(3, 4).mag == 5.0f);
207     assert(Vec2d.empty.mag == 0);
208     assert(Vec2d(1.0, 1.0).dot(Vec2d(1.0, 1.0)) == 2.0);
209 
210     // Test floating-point specific methods.
211     auto v6 = Vec2f(3, 3);
212     v6.norm();
213     assertFP(v6.mag, 1);
214     assertFP(v6[0], sqrt(2f) / 2f);
215     v6 = Vec2f(1, 0);
216     auto v6Copy = Vec2f(v6);
217     v6.norm();
218     assert(v6.mag == 1);
219     assert(v6.data == v6Copy.data);
220 
221     // Test toPolar
222     auto vCart = Vec2d(1, 0);
223     vCart.toPolar();
224     assert(vCart.data == [1.0, 0.0]);
225     vCart.toCartesian();
226     assert(vCart.data == [1.0, 0.0]);
227     vCart = Vec2d(1, 1);
228     vCart.toPolar();
229     assertFP(vCart[0], sqrt(2f));
230     assertFP(vCart[1], PI_4);
231     vCart.toCartesian();
232     assertFP(vCart[0], 1);
233     assertFP(vCart[1], 1);
234     vCart = Vec2d(-1, 1);
235     vCart.toPolar();
236     assertFP(vCart[0], sqrt(2f));
237     assertFP(vCart[1], 3 * PI_4);
238 }