Skip to main content

hexx/
layout.rs

1use crate::{EdgeDirection, Hex, HexOrientation, VertexDirection, orientation::SQRT_3};
2use glam::Vec2;
3
4/// Hexagonal layout. This type is the bridge between your *world*/*pixel*
5/// coordinate system and the hexagonal coordinate system.
6///
7/// # Axis
8///
9/// By default, the [`Hex`] `y` axis is pointing up and the `x` axis is
10/// pointing right but you have the option to invert them using `invert_x` and
11/// `invert_y` This may be useful depending on the coordinate system of your
12/// display.
13///
14/// # Example
15///
16/// ```rust
17/// # use hexx::*;
18///
19/// let layout = HexLayout {
20///     // We want flat topped hexagons
21///     orientation: HexOrientation::Flat,
22///     // We define the world space origin equivalent of `Hex::ZERO` in hex space
23///     origin: Vec2::new(1.0, 2.0),
24///     // We define the world space scale of the hexagons
25///     scale: Vec2::new(1.0, 1.0),
26/// };
27/// // You can now find the world positon (center) of any given hexagon
28/// let world_pos = layout.hex_to_world_pos(Hex::ZERO);
29/// // You can also find which hexagon is at a given world/screen position
30/// let hex_pos = layout.world_pos_to_hex(Vec2::new(1.23, 45.678));
31/// ```
32///
33/// # Builder
34///
35/// `HexLayout` provides a builder pattern:
36///
37/// ```rust
38/// # use hexx::*;
39///
40/// let mut layout = HexLayout::flat()
41///     .with_scale(Vec2::new(2.0, 3.0)) // Individual Hexagon size
42///     .with_origin(Vec2::new(-1.0, 0.0)); // World origin
43/// // Invert the x axis, which will now go left. Will change `scale.x` to `-2.0`
44/// layout.invert_x();
45/// // Invert the y axis, which will now go down. Will change `scale.y` to `-3.0`
46/// layout.invert_y();
47/// ```
48///
49/// ## Working with Sprites
50///
51/// If you intend to use the hexagonal grid to place images/sprites you may use
52/// `HexLayout::with_rect_size` to make the hexagon scale fit the your sprite
53/// dimensions.
54///
55/// You can also retrieve the matching rect size from any layout using
56/// `HexLayout::rect_size()`
57#[derive(Debug, Clone)]
58#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59// #[cfg_attr(feature = "facet", derive(facet::Facet))]
60#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))]
61#[cfg_attr(
62    feature = "bevy_ecs",
63    derive(bevy_ecs::resource::Resource, bevy_ecs::component::Component)
64)]
65pub struct HexLayout {
66    /// The hexagonal orientation of the layout (usually "flat" or "pointy")
67    pub orientation: HexOrientation,
68    /// The origin of the hexagonal representation in world/pixel space, usually
69    /// [`Vec2::ZERO`]
70    pub origin: Vec2,
71    /// The size of individual hexagons in world/pixel space. The scale can be
72    /// irregular or negative
73    pub scale: Vec2,
74}
75
76impl HexLayout {
77    /// Inverts the layout `X` axis
78    pub fn invert_x(&mut self) {
79        self.scale.x *= -1.0;
80    }
81
82    /// Inverts the layout `Y` axis
83    pub fn invert_y(&mut self) {
84        self.scale.y *= -1.0;
85    }
86
87    /// Transforms a local hex space vector to world space
88    /// by applying the layout `scale` but NOT the origin
89    #[must_use]
90    #[inline]
91    pub fn transform_vector(&self, vector: Vec2) -> Vec2 {
92        vector * self.scale
93    }
94
95    /// Transforms a local hex point to world space
96    /// by applying the layout `scale` and `origin`
97    #[must_use]
98    #[inline]
99    pub fn transform_point(&self, point: Vec2) -> Vec2 {
100        self.origin + self.transform_vector(point)
101    }
102
103    /// Transforms a world space vector to local hex space
104    /// by applying the layout `scale` but NOT the origin
105    #[must_use]
106    #[inline]
107    pub fn inverse_transform_vector(&self, vector: Vec2) -> Vec2 {
108        vector / self.scale
109    }
110
111    /// Transforms a world pace point to local hex space
112    /// by applying the layout `scale` and `origin`
113    #[must_use]
114    #[inline]
115    pub fn inverse_transform_point(&self, point: Vec2) -> Vec2 {
116        self.inverse_transform_vector(point - self.origin)
117    }
118}
119
120impl HexLayout {
121    #[must_use]
122    #[inline]
123    /// Computes hexagonal coordinates `hex` into world/pixel coordinates
124    pub fn hex_to_world_pos(&self, hex: Hex) -> Vec2 {
125        self.hex_to_center_aligned_world_pos(hex) + self.origin
126    }
127
128    #[must_use]
129    #[inline]
130    /// Computes hexagonal coordinates `hex` into world/pixel coordinates but
131    /// ignoring [`HexLayout::origin`]
132    pub(crate) fn hex_to_center_aligned_world_pos(&self, hex: Hex) -> Vec2 {
133        let p = self.orientation.forward(hex.as_vec2());
134        self.transform_vector(p)
135    }
136
137    #[must_use]
138    #[inline]
139    /// Computes fractional hexagonal coordinates `hex` into world/pixel
140    /// coordinates
141    pub fn fract_hex_to_world_pos(&self, hex: Vec2) -> Vec2 {
142        let p = self.orientation.forward(hex);
143        self.transform_point(p)
144    }
145
146    #[must_use]
147    #[inline]
148    /// Computes world/pixel coordinates `pos` into hexagonal coordinates
149    pub fn world_pos_to_hex(&self, pos: Vec2) -> Hex {
150        let p = self.world_pos_to_fract_hex(pos).to_array();
151        Hex::round(p)
152    }
153
154    #[must_use]
155    /// Computes world/pixel coordinates `pos` into fractional hexagonal
156    /// coordinates
157    pub fn world_pos_to_fract_hex(&self, pos: Vec2) -> Vec2 {
158        let point = self.inverse_transform_point(pos);
159        self.orientation.inverse(point)
160    }
161
162    #[must_use]
163    /// Retrieves all 6 corner coordinates of the given hexagonal coordinates
164    /// `hex`
165    pub fn hex_corners(&self, hex: Hex) -> [Vec2; 6] {
166        let center = self.hex_to_world_pos(hex);
167        self.center_aligned_hex_corners().map(|c| c + center)
168    }
169
170    /// Retrieves all 6 edge corner pair coordinates of the given hexagonal
171    /// coordinates `hex`
172    #[must_use]
173    pub fn hex_edge_corners(&self, hex: Hex) -> [[Vec2; 2]; 6] {
174        let center = self.hex_to_world_pos(hex);
175        self.center_aligned_edge_corners()
176            .map(|p| p.map(|c| c + center))
177    }
178
179    #[must_use]
180    /// Retrieves all 6 edge corner pair coordinates of the given hexagonal
181    /// coordinates `hex` without offsetting at the origin
182    pub fn center_aligned_hex_corners(&self) -> [Vec2; 6] {
183        VertexDirection::ALL_DIRECTIONS.map(|dir| dir.world_unit_vector(self))
184    }
185
186    #[must_use]
187    /// Non offsetted hex edges
188    pub(crate) fn center_aligned_edge_corners(&self) -> [[Vec2; 2]; 6] {
189        EdgeDirection::ALL_DIRECTIONS
190            .map(|dir| dir.vertex_directions().map(|v| v.world_unit_vector(self)))
191    }
192
193    #[inline]
194    #[must_use]
195    /// Returns the size of the bounding box/rect of an hexagon
196    /// This uses both the `hex_size` and `orientation` of the layout.
197    pub fn rect_size(&self) -> Vec2 {
198        const FLAT_RECT: Vec2 = Vec2::new(2.0, SQRT_3);
199        const POINTY_RECT: Vec2 = Vec2::new(SQRT_3, 2.0);
200
201        self.scale
202            * match self.orientation {
203                HexOrientation::Pointy => POINTY_RECT,
204                HexOrientation::Flat => FLAT_RECT,
205            }
206    }
207}
208
209#[cfg(feature = "grid")]
210impl HexLayout {
211    /// Returns the  world coordinate of the two edge vertices in clockwise
212    /// order
213    #[must_use]
214    pub fn edge_coordinates(&self, edge: crate::GridEdge) -> [Vec2; 2] {
215        let origin = self.hex_to_world_pos(edge.origin);
216        edge.vertices()
217            .map(|v| self.__vertex_coordinates(v) + origin)
218    }
219
220    /// Returns the  world coordinate of all edge vertex pairs in clockwise
221    /// order
222    #[must_use]
223    pub fn all_edge_coordinates(&self, coord: Hex) -> [[Vec2; 2]; 6] {
224        let origin = self.hex_to_world_pos(coord);
225        coord.all_edges().map(|edge| {
226            edge.vertices()
227                .map(|v| self.__vertex_coordinates(v) + origin)
228        })
229    }
230
231    /// Returns the world coordinate of the vertex
232    #[must_use]
233    pub fn vertex_coordinates(&self, vertex: crate::GridVertex) -> Vec2 {
234        let origin = self.hex_to_world_pos(vertex.origin);
235        self.__vertex_coordinates(vertex) + origin
236    }
237
238    fn __vertex_coordinates(&self, vertex: crate::GridVertex) -> Vec2 {
239        vertex.direction.world_unit_vector(self)
240    }
241}
242
243// Builder pattern
244impl HexLayout {
245    #[must_use]
246    #[inline]
247    /// Constructs a new layout with the given `orientation` and default
248    /// values
249    pub const fn new(orientation: HexOrientation) -> Self {
250        Self {
251            orientation,
252            origin: Vec2::ZERO,
253            scale: Vec2::ONE,
254        }
255    }
256
257    #[must_use]
258    #[inline]
259    /// Constructs a new flat layout with default
260    /// values
261    pub const fn flat() -> Self {
262        Self::new(HexOrientation::Flat)
263    }
264
265    #[must_use]
266    #[inline]
267    /// Constructs a new pointylayout with default
268    /// values
269    pub const fn pointy() -> Self {
270        Self::new(HexOrientation::Pointy)
271    }
272
273    #[must_use]
274    #[inline]
275    /// Specifies the world/pixel origin of the layout
276    pub const fn with_origin(mut self, origin: Vec2) -> Self {
277        self.origin = origin;
278        self
279    }
280
281    #[must_use]
282    #[inline]
283    /// Specifies the world/pixel regular size of individual hexagons
284    pub const fn with_hex_size(mut self, size: f32) -> Self {
285        self.scale = Vec2::splat(size);
286        self
287    }
288
289    #[inline]
290    #[must_use]
291    /// Specifies the world/pixel size of individual hexagons to match
292    /// the given `rect_size`. This is useful if you want hexagons
293    /// to match a sprite size
294    pub fn with_rect_size(self, rect_size: Vec2) -> Self {
295        const FLAT_RECT: Vec2 = Vec2::new(0.5, 1.0 / SQRT_3);
296        const POINTY_RECT: Vec2 = Vec2::new(1.0 / SQRT_3, 0.5);
297
298        let scale = rect_size
299            * match self.orientation {
300                HexOrientation::Pointy => POINTY_RECT,
301                HexOrientation::Flat => FLAT_RECT,
302            };
303        self.with_scale(scale)
304    }
305
306    #[must_use]
307    #[inline]
308    /// Specifies the world/pixel scale of individual hexagons.
309    ///
310    /// # Note
311    ///
312    /// For most use cases prefer [`Self::with_hex_size`] instead.
313    pub const fn with_scale(mut self, scale: Vec2) -> Self {
314        self.scale = scale;
315        self
316    }
317}
318
319impl Default for HexLayout {
320    #[inline]
321    fn default() -> Self {
322        Self::new(HexOrientation::default())
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use approx::assert_relative_eq;
329
330    use super::*;
331
332    #[test]
333    fn flat_corners() {
334        let point = Hex::new(0, 0);
335        let mut layout = HexLayout::new(HexOrientation::Flat).with_scale(Vec2::new(10., 10.));
336        let corners = layout.hex_corners(point).map(Vec2::round);
337        assert_eq!(
338            corners,
339            [
340                Vec2::new(10.0, 0.0),
341                Vec2::new(5.0, 9.0),
342                Vec2::new(-5.0, 9.0),
343                Vec2::new(-10.0, 0.0),
344                Vec2::new(-5.0, -9.0),
345                Vec2::new(5.0, -9.0),
346            ]
347        );
348        layout.invert_y();
349        let corners = layout.hex_corners(point).map(Vec2::round);
350        assert_eq!(
351            corners,
352            [
353                Vec2::new(10.0, 0.0),
354                Vec2::new(5.0, -9.0),
355                Vec2::new(-5.0, -9.0),
356                Vec2::new(-10.0, 0.0),
357                Vec2::new(-5.0, 9.0),
358                Vec2::new(5.0, 9.0),
359            ]
360        );
361    }
362
363    #[test]
364    fn pointy_corners() {
365        let point = Hex::new(0, 0);
366        let mut layout = HexLayout::new(HexOrientation::Pointy).with_scale(Vec2::new(10., 10.));
367        let corners = layout.hex_corners(point).map(Vec2::round);
368        assert_eq!(
369            corners,
370            [
371                Vec2::new(9.0, -5.0),
372                Vec2::new(9.0, 5.0),
373                Vec2::new(-0.0, 10.0),
374                Vec2::new(-9.0, 5.0),
375                Vec2::new(-9.0, -5.0),
376                Vec2::new(0.0, -10.0),
377            ]
378        );
379        layout.invert_y();
380        let corners = layout.hex_corners(point).map(Vec2::round);
381        assert_eq!(
382            corners,
383            [
384                Vec2::new(9.0, 5.0),
385                Vec2::new(9.0, -5.0),
386                Vec2::new(-0.0, -10.0),
387                Vec2::new(-9.0, -5.0),
388                Vec2::new(-9.0, 5.0),
389                Vec2::new(0.0, 10.0),
390            ]
391        );
392    }
393
394    #[test]
395    fn rect_size() {
396        let sizes = [
397            Vec2::ZERO,
398            Vec2::ONE,
399            Vec2::X,
400            Vec2::Y,
401            Vec2::NEG_ONE,
402            Vec2::NEG_X,
403            Vec2::NEG_Y,
404            Vec2::new(10.0, 5.0),
405            Vec2::new(-10.0, 31.1),
406            Vec2::new(110.0, 25.0),
407            Vec2::new(-210.54, -54.0),
408        ];
409        for size in sizes {
410            for orientation in [HexOrientation::Flat, HexOrientation::Pointy] {
411                let layout = HexLayout::new(orientation).with_rect_size(size);
412                let rect = layout.rect_size();
413                assert_relative_eq!(rect.x, size.x, epsilon = 0.00001);
414                assert_relative_eq!(rect.y, size.y, epsilon = 0.00001);
415            }
416        }
417    }
418}