1 module isodi.resources.loader;
2 
3 import raylib;
4 import std.algorithm;
5 
6 import isodi.chunk;
7 import isodi.utils;
8 import isodi.skeleton;
9 
10 
11 @safe:
12 
13 
14 /// Interface for resource loading, can be specified in the `Properties` of each object.
15 ///
16 /// Use `Pack` and `getPack` to use Isodi's default pack loader.
17 ///
18 /// The loader should be responsible for allocating, caching and freeing the textures.
19 interface ResourceLoader {
20 
21     /// Get options for the given resource.
22     const(ResourceOptions)* options(ResourceType resource, string name);
23 
24     /// Load texture for a chunk.
25     Texture2D blockTexture(string[] names, out BlockUV[BlockType] uv)
26     in (isSorted(names));
27 
28     ///// Load texture for the given bone sets.
29     Texture2D boneSetTexture(string[] names, out BoneUV[BoneType] uv)
30     in (isSorted(names));
31 
32     /// Load bones for a skeleton, using the given bone set.
33     Bone[] skeleton(string name, string boneSet);
34 
35     // TODO packImages wrapper to support variably sized images.
36 
37     /// Combine the given images to create an atlas map. Each image must be square with dimensions equal to a power of
38     /// two, eg. 32×32 or 128x128. All images must be of equal size.
39     ///
40     /// UV type must be a `RectangleI` or struct containing `RectangleI`s. The new mapping will offset the positions
41     /// of those rectangles to match those of the newly made image.
42     ///
43     /// If given only one image, returns it unchanged.
44     static Image packImages(UV, Type)(return scope UV[Type][] mapping, return scope Image[] images,
45         out UV[Type] newMapping)
46     in (images.length != 0,
47         "No images given")
48     in (images.all!"a.width == a.height && !(a.width & (a.width-1))",
49         "All images are required to be square with dimensions equal to a power of two, eg. 32x32")
50     in (images.isSorted!"a.width > b.width",
51         "Given images must be sorted descending by size")
52     in (mapping.length == images.length,
53         "Map and image count must be equal")
54     out (r; r.width == r.height,
55         "Output image must be square")
56     out (r; !(r.width & (r.width - 1)),
57         "Output image must have dimensions equal to a power of two")
58     do {
59 
60         import std.math, std.range, std.traits;
61 
62         static assert(is(UV == struct), "Mapping type must be a struct");
63 
64         T offsetUV(T)(T uv, int x, int y) {
65 
66             // Given a rectangle, offset it
67             static if (is(T == RectangleI)) {
68 
69                 return RectangleI(uv.x + x, uv.y + y, uv.width, uv.height);
70 
71             }
72 
73             // Something else!
74             else {
75 
76                 // Check each field of the struct
77                 foreach (ref field; uv.tupleof) {
78 
79                     // Search for rectangles
80                     static if (is(typeof(field) == RectangleI)) {
81 
82                         // Apply offset to them
83                         field = offsetUV(field, x, y);
84 
85                     }
86 
87                 }
88 
89                 return uv;
90 
91             }
92 
93         }
94 
95         // Just one image
96         if (mapping.length == 1) {
97 
98             // Nothing to do
99             newMapping = mapping[0];
100             return images[0];
101 
102         }
103 
104         // Allocate the new image
105         const imagesPerRow = cast(int) sqrt(cast(real) mapping.length).ceil;
106         const partSize = images[0].width;
107         const size = partSize * imagesPerRow;
108 
109         Image result = {
110             data: &(new Color[size * size])[0],
111             width: size,
112             height: size,
113             mipmaps: 1,
114             format: PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
115         };
116 
117         int i, j;
118 
119         foreach (map, image; zip(mapping, cast(const(Image)[]) images)) {
120 
121             // Advance to the next spot
122             scope (success) {
123                 i += 1;
124                 j += i / imagesPerRow;
125                 i %= imagesPerRow;
126             }
127 
128             const offsetX = partSize*i;
129             const offsetY = partSize*j;
130 
131             // Update the mapping
132             foreach (key, value; map) {
133 
134                 newMapping[key] = offsetUV(value, offsetX, offsetY);
135 
136             }
137 
138             // Place the image
139             () @trusted {
140 
141                 // Note: ImageDraw is very imprecise
142 
143                 // Copy the pixels over
144                 foreach (y; 0 .. partSize)
145                 foreach (x; 0 .. partSize) {
146 
147                     ImageDrawPixel(&result, offsetX + x, offsetY + y, GetImageColor(cast() image, x, y));
148 
149                 }
150 
151             }();
152 
153         }
154 
155         return result;
156 
157     }
158 
159     unittest {
160 
161         Color[2][2] colorsA = [
162             [Color(1, 1, 1, 1), Color(2, 2, 2, 2)],
163             [Color(3, 3, 3, 3), Color(4, 4, 4, 4)],
164         ];
165         Color[2][2] colorsB = [
166             [Color(5, 5, 5, 5), Color(6, 6, 6, 6)],
167             [Color(7, 7, 7, 7), Color(8, 8, 8, 8)],
168         ];
169 
170         scope Image imageA = {
171             data: &colorsA,
172             width: 2,
173             height: 2,
174             mipmaps: 1,
175             format: PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
176         };
177         scope Image imageB = {
178             data: &colorsB,
179             width: 2,
180             height: 2,
181             mipmaps: 1,
182             format: PixelFormat.PIXELFORMAT_UNCOMPRESSED_R8G8B8A8,
183         };
184 
185         RectangleI[string] mapA = [
186             "a": RectangleI(0, 0, 1, 2),
187         ];
188         RectangleI[string] mapB = [
189             "b": RectangleI(0, 0, 1, 2),
190             "c": RectangleI(1, 0, 1, 2),
191         ];
192         RectangleI[string] map;
193 
194         auto image = packImages([mapA, mapB], [imageA, imageB], map);
195 
196         () @trusted {
197 
198             assert(image.GetImageColor(0, 0) == Color(1, 1, 1, 1));
199             assert(image.GetImageColor(1, 0) == Color(2, 2, 2, 2));
200             assert(image.GetImageColor(2, 0) == Color(5, 5, 5, 5));
201             assert(image.GetImageColor(3, 0) == Color(6, 6, 6, 6));
202             assert(image.GetImageColor(3, 1) == Color(8, 8, 8, 8));
203             assert(image.GetImageColor(3, 3) == Color(0, 0, 0, 0));
204 
205         }();
206 
207         assert(map == [
208             "a": RectangleI(0, 0, 1, 2),
209             "b": RectangleI(2, 0, 1, 2),
210             "c": RectangleI(3, 0, 1, 2),
211         ]);
212 
213     }
214 
215 }
216 
217 /// Type of the resource.
218 enum ResourceType {
219 
220     block,
221     bone,
222     skeleton,
223 
224 }
225 
226 /// Resource options
227 struct ResourceOptions {
228 
229     /// If true, a filter will be applied to smooth out the texture. This should be off for pixel art packs.
230     bool interpolate = true;
231 
232     // TODO better docs
233 
234     /// Size of the tile texture (both width and height).
235     ///
236     /// Required.
237     uint tileSize;
238 
239     /// Side texture height.
240     uint sideSize;
241 
242     /// Amount of angles each multi-directional texture will provide. All angles should be placed in a single
243     /// row in the image.
244     ///
245     /// 4 angles means the textures have a separate sprite for every 90 degrees, 8 angles — 45 degrees,
246     /// and so on.
247     ///
248     /// Defaults to `4`.
249     uint angles = 4;
250 
251     int[4] tileArea;
252     int[4] sideArea;
253 
254     auto blockUV() const => BlockUV(
255         cast(RectangleI) tileArea,
256         cast(RectangleI) sideArea,
257         tileSize,
258         sideSize,
259     );
260 
261 }