1 ///
2 module isodi.raylib.display;
3 
4 import std.conv;
5 import std.math;
6 import std.typecons;
7 import std.container;
8 import std.algorithm;
9 
10 import raylib;
11 
12 import isodi.bind;
13 import isodi.display;
14 import isodi.object3d;
15 import isodi.position;
16 import isodi.resource;
17 import isodi.raylib.cell;
18 import isodi.raylib.anchor;
19 import isodi.raylib.internal;
20 
21 
22 @safe:
23 
24 
25 ///
26 final class RaylibDisplay : Display {
27 
28     /// Underlying raylib camera.
29     package raylib.Camera raycam;
30 
31     ///
32     this() {
33 
34         // Set camera constants
35         raycam.up = Vector3(0.0, 1.0, 0.0);
36         raycam.projection = CameraProjection.CAMERA_ORTHOGRAPHIC;
37 
38     }
39 
40     /// Get the underlying Raylib camera.
41     const(raylib.Camera) raylibCamera() const {
42 
43         return raycam;
44 
45     }
46 
47     override void reloadResources() {
48 
49         packs.clearCache();
50         foreach (cell; cells) cell.reload();
51         foreach (model; models) model.reload();
52 
53     }
54 
55     /// Get Isodi position from world position.
56     /// Params:
57     ///     original = Raylib world position.
58     Position isodiPosition(Vector3 original) const {
59 
60         return position(
61             cast(int) floor(original.x / cellSize),
62             cast(int) floor(original.z / cellSize),
63             Height(original.y / cellSize),
64         );
65 
66     }
67 
68     /// Get a ray from the mouse position relative to the camera.
69     /// Params:
70     ///     inverted = Shoots the ray behind the camera. It might be useful to check both the normal and inverted ray
71     ///         the ray doesn't recognize camera height properly.
72     Ray mouseRay(bool inverted) const @trusted {
73 
74         auto ray = GetMouseRay(GetMousePosition, raycam);
75 
76         if (inverted) {
77 
78             ray.direction = Vector3Negate(ray.direction);
79 
80         }
81 
82         return ray;
83 
84     }
85 
86     /// Snap a Vector3 with a world position to Isodi.
87     Vector3 snapWorldPosition(Vector3 pos) const {
88 
89         return Vector3(
90             cast(int) pos.x / cellSize * cellSize,
91             cast(int) pos.y / cellSize * cellSize,
92             cast(int) pos.z / cellSize * cellSize,
93         );
94 
95     }
96 
97     /// Draw the contents of the display.
98     ///
99     /// Must be called inside `DrawingMode`, but not `BeginMode3D`.
100     void draw() @trusted {
101 
102         updateCamera();
103 
104         // Draw
105         BeginMode3D(raycam);
106         scope (exit) EndMode3D();
107 
108         import std.array : array;
109         import std.range : chain;
110 
111         rlOrtho(-1, 1, -1, 1, 0.01, cellSize * cellSize);
112         rlDisableDepthTest();
113 
114         alias PI = std.math.PI;
115 
116         const rad = PI / 180;
117         const radX = camera.angle.x * rad;
118         const radY = camera.angle.y * rad;
119 
120         // Get perceived screen size based on camera angle
121         // 90° = ×1, 0° = ×∞
122         // No idea what would be the best formula here, at first I used tan, but it turned out to be excessive.
123         // This seems to work well...
124         const screenWidth  = GetScreenWidth;
125         const screenHeight = cast(int) (GetScreenHeight * sqrt(1 + radY));
126 
127         // Get all 3D objects
128         chain(
129             cells.map!(a => cameraDistance(a, radX, 0)),
130             models.map!(a => cameraDistance(a, radX, 1)),
131             anchors.map!(a => cameraDistance(a, radX, 2, (cast(RaylibAnchor) a).drawOrder))
132         )
133 
134             // Ignore invisible objects
135             .filter!(a => inBounds(a, screenWidth, screenHeight))
136 
137             // Depth sort
138             .array
139             .multiSort!(`a[1] < b[1]`, `a[2] > b[2]`, `a[3] < b[3]`, `a[4] < b[4]`)
140 
141             // Draw them
142             .each!(a => a[0].to!WithDrawableResources.draw());
143 
144     }
145 
146     private alias SortTuple = Tuple!(Object3D, RaylibAnchor.DrawOrder, float, float, uint);
147 
148     /// Check if the object is in bounds of the display.
149     private bool inBounds(SortTuple object, int screenWidth, int screenHeight) {
150 
151         Vector2 screenPoint(CellPoint point) @trusted {
152 
153             return GetWorldToScreen(object[0].position.toVector3(cellSize, point), raycam);
154 
155         }
156 
157         const center = screenPoint(CellPoint.center);
158         const edge   = screenPoint(CellPoint.edge);
159 
160         const diagX = abs(edge.x - center.x);
161         const diagY = abs(edge.y - center.y);
162 
163         // Top is visible
164         if (0 <= center.x + diagX && center.x - diagX < screenWidth
165          && 0 <= center.y + diagY && center.y - diagY < screenHeight) {
166 
167             return true;
168 
169         }
170 
171         // Get bottom position
172         const bottom = screenPoint(CellPoint.bottomCenter);
173 
174         // Bottom is visible
175         if (0 <= bottom.x + diagX && bottom.x - diagX < screenWidth
176          && 0 <= bottom.y + diagY && bottom.y - diagY < screenHeight) {
177 
178             return true;
179 
180         }
181 
182         // Nope, nothing is visible
183         return false;
184 
185     }
186 
187     /// Get the camera distance of given Object3D
188     private SortTuple cameraDistance(Object3D object, real rad, uint priority,
189     RaylibAnchor.DrawOrder order = RaylibAnchor.DrawOrder.position) {
190 
191         return SortTuple(
192             object,
193             order,
194             -object.visualPosition.x * sin(rad)
195               - object.visualPosition.y * cos(rad),
196             object.visualPosition.height.top,
197             priority,
198         );
199 
200     }
201 
202     private void updateCamera() {
203 
204         const rad = std.math.PI / 180;
205         const radX = camera.angle.x * rad;
206         const radY = camera.angle.y * rad;
207         const cosY = cos(camera.angle.y * rad);
208 
209         // Calculate the target
210         const target = camera.follow is null
211             ? Position()
212             : camera.follow.visualPosition;
213         const targetVector = target.toVector3(cellSize, CellPoint.center);
214 
215         // Update the camera
216         // not sure how to get the correct fovy from distance, this is just close to the expected result.
217         // TODO: figure it out.
218         raycam.fovy = camera.distance * cellSize;
219 
220         // Get the target
221         raycam.target = Vector3(
222             targetVector.x + camera.offset.x * cellSize,
223             targetVector.y + camera.offset.height * cellSize,
224             targetVector.z + camera.offset.y * cellSize,
225         );
226 
227         // And place the camera
228         raycam.position = raycam.target + Vector3(
229             radX.sin * cosY,
230             radY.sin,
231             radX.cos * cosY,
232         ) * camera.distance;
233 
234     }
235 
236     /// Add a Raylib anchor. See `isodi.Anchor` for reference.
237     /// Params:
238     ///     callback = Function that will be called every frame in order to draw the anchor content.
239     /// Returns: The created anchor
240     RaylibAnchor addAnchor(void delegate() @trusted callback) {
241 
242         auto anchor = cast(RaylibAnchor) super.addAnchor();
243         anchor.callback = callback;
244         return anchor;
245 
246     }
247 
248 }