1 ///
2 module isodi.display;
3 
4 import isodi.bind;
5 import isodi.cell;
6 import isodi.pack;
7 import isodi.tests;
8 import isodi.anchor;
9 import isodi.camera;
10 import isodi.position;
11 
12 
13 @safe:
14 
15 
16 /// Display is the main class of Isodi which manages all Isodi resources.
17 ///
18 /// This class is abstract, as it should be overriden by the renderer. Use the `make` method to create a display with
19 /// the current renderer.
20 ///
21 /// Note: If the display is destroyed, all of its members will automatically be destroyed too, so references to display
22 /// objects will be dead. Make sure you clean them out before switching displays.
23 abstract class Display {
24 
25     public {
26 
27         /// Base cell size in the display, if supported by the renderer.
28         ///
29         /// Usually in pixels.
30         int cellSize = 100;
31 
32         /// Active camera.
33         Camera camera;
34 
35         /// Used pack list
36         PackList packs;
37 
38     }
39 
40     protected {
41 
42         /// Registered cells
43         Cell[UniquePosition] cellMap;
44 
45         /// Registered anchors
46         Anchor[size_t] anchorMap;
47 
48         /// Registered models
49         Model[size_t] modelMap;
50 
51     }
52 
53     private {
54 
55         /// ID of the last added anchor
56         size_t lastAnchor;
57 
58         /// ID of the last added model
59         size_t lastModel;
60 
61     }
62 
63     ///
64     this() {
65 
66         packs = PackList.make();
67 
68     }
69 
70     ~this() @system {
71 
72         clearDestroy();
73 
74     }
75 
76     /// Create a display with the current renderer.
77     static Display make() {
78 
79         return Renderer.createDisplay();
80 
81     }
82 
83     /// Reload all resources in the display. Make sure to call `PackList.clearCache`.
84     abstract void reloadResources();
85 
86     /// Iterate on all cells
87     auto cells() { return cellMap.byValue; }
88 
89     /// Iterate on all models
90     auto models() { return modelMap.byValue; }
91 
92     /// Iterate on all anchors
93     auto anchors() { return anchorMap.byValue; }
94 
95     /// Get the number of cells in the display.
96     size_t cellCount() { return cellMap.length; }
97 
98     /// Get the number of models in the display.
99     size_t modelCount() { return modelMap.length; }
100 
101     /// Get the number of anchors in the display.
102     size_t anchorCount() { return anchorMap.length; }
103 
104     /// Clear all objects within the display. Leaves packs and cache in place.
105     void clear() @system {
106 
107         cellMap.clear();
108         modelMap.clear();
109         anchorMap.clear();
110 
111     }
112 
113     /// Destroy the contents of the display.
114     void clearDestroy() @system {
115 
116         // Destroy all resources
117         foreach (cell; cells) cell.destroy();
118         foreach (model; models) model.destroy();
119         foreach (anchor; anchors) anchor.destroy();
120 
121         clear();
122 
123     }
124 
125     /// Add a new cell to the display. Replaces the cell if one already exists.
126     /// Params:
127     ///     position = Position of the cell in the display.
128     ///     type     = Type of the cell.
129     /// Returns: The created cell.
130     Cell addCell(const Position position, const string type) {
131 
132         auto cell = Cell.make(this, position, type);
133         cellMap[position.toUnique] = cell;
134         return cell;
135 
136     }
137 
138     /// Get a cell at given position.
139     /// Params:
140     ///     position = Position of the cell.
141     /// Returns: The cell at this position. `null` if not found.
142     Cell getCell(const UniquePosition position) {
143 
144         return cellMap.get(position, null);
145 
146     }
147 
148     /// Remove the cell at given position.
149     ///
150     /// Returns: True if the cell was actually removed.
151     bool removeCell(UniquePosition position) {
152 
153         return cellMap.remove(position);
154 
155     }
156 
157     /// Ditto.
158     bool removeCell(Position position) {
159 
160         return removeCell(position.toUnique);
161 
162     }
163 
164     unittest {
165 
166         auto display = TestRunner.makeDisplay;
167 
168         // Add two sample cells
169         display.addCell(position(1, 2), "wood");
170         display.addCell(position(2, 2), "wood");
171 
172         // Replace the first cell
173         display.addCell(position(1, 2), "grass");
174 
175         // Add a cell on a different layer
176         display.addCell(position(2, 2, 1), "grass");
177 
178         assert(display.getCell(UniquePosition(1, 2, 0)).type == "grass");
179         assert(display.getCell(UniquePosition(2, 2, 0)).type == "wood");
180         assert(display.getCell(UniquePosition(3, 2, 0)) is null);
181 
182         // Remove the first cell
183         display.removeCell(position(1, 2));
184 
185         assert(display.getCell(UniquePosition(1, 2, 0)) is null);
186 
187     }
188 
189     /// Add a new model to the display.
190     /// Params:
191     ///     position = Position to place the model on.
192     ///     type     = Skeleton for the model
193     Model addModel(const string type) {
194 
195         // Create the model
196         auto model = Model.make(this, type);
197         modelMap[model.id] = model;
198 
199         return model;
200 
201     }
202 
203     /// Ditto
204     Model addModel(Position position, const string type) {
205 
206         auto model = addModel(type);
207         model.position = position;
208         return model;
209 
210     }
211 
212     /// Remove the given model from the map.
213     /// Returns: True if the model was actually removed.
214     bool removeModel(Model model) {
215 
216         return modelMap.remove(model.id);
217 
218     }
219 
220     /// Add a new anchor to the display.
221     /// Returns: The created anchor.
222     Anchor addAnchor(Position position = Position.init) {
223 
224         // Create the anchor
225         auto anchor = Anchor.make(this);
226         anchorMap[anchor.id] = anchor;
227 
228         // Set the position
229         anchor.position = position;
230 
231         return anchor;
232 
233     }
234 
235     /// Remove the given anchor from the map.
236     /// Returns: True if the anchor was actually removed.
237     bool removeAnchor(Anchor anchor) {
238 
239         return anchorMap.remove(anchor.id);
240 
241     }
242 
243 }
244 
245 mixin DisplayTest!((display) {
246 
247     // Add a few cells
248     display.addCell(position(1, 1), "grass");
249     display.addCell(position(1, 2), "grass");
250     display.addCell(position(1, 3), "grass");
251 
252     // Place models on them
253     display.addModel(position(1, 1), "wraith-white");
254     display.addModel(position(1, 2), "wraith-white");
255     auto a = display.addModel(position(1, 3), "wraith-white");
256 
257     // Remove the third and fourth cell (different methods)
258     display.removeCell(position(1, 3));
259 
260     // Remove the third and fourth models
261     display.removeModel(a);
262 
263     // Note: now the model can be safely deleted
264     (() @trusted => a.destroy)();
265 
266     // Also: need to test anchors, but there isn't really a visible way... should probably be handled by the renderer
267     // still TODO
268 
269 });