Skip to content

Commit

Permalink
Axis inconsistency (#112)
Browse files Browse the repository at this point in the history
> Closes #110 

# Context (#110)

Currently the `Hex` coordinate system follows the convention that `y` is
*going down*, for flat hexagons mostly as pointy hexagons it would go
*down-right*. This is inconsistent:
- with the 2d Y-up convention of game engines like **Bevy**, this is not
a big issue, as the hex coords don't relate directly to pixel
coordinates
- with the `Direction` enum, `Direction::Top` is equivalent of `hex(0,
-1)` and `Direction::Bottom`hex(0, 1)` which is a major inconsistency
with the coordinate system.

~~This PR provides a fix with 1e1103c
which does the following:
Invert the `y` axis in `HexLayout::hex_to_world_pos` and
`HexLayout::world_pos_to_hex`. This is a debatable breaking change~~
This Pr provides a fix with 47ce481
which does the following:
Adds the option to invert each axis in `HexLayout`, keeping the current
behaviour as default to avoid breaking change

Other solutions:
- [ ] ~~Keep the *y up* as it is but rename `Direction` variants to
correctly represent the orientation~~
- [ ] ~~Keep the *y up* but rename `Direction` variants based on axis
instead of orientation~~
- [x] Allow customization in `HexLayout` with something like `y_up:
bool` which is probably a good idea
  • Loading branch information
ManevilleF authored Aug 6, 2023
1 parent 8744602 commit a88a6ec
Show file tree
Hide file tree
Showing 13 changed files with 94 additions and 29 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [Unreleased]

* Examples use camera viewport to retrieve cursor world position (#112)
* (**BREAKING**) `Hex::round` now takes a `[f32; 2]` parameter instead of
`(f32, f32)` (#112)
* `HexLayout` now has the option to invert `Hex` x and y axis (#110, #112)
* (**BREAKING**) `Hex::y` axis now correctly points towards the negative world/
pixel/screen `y` coordinates

## 0.9.2

* Fixed some documentation on `Direction` (#111)
Expand Down
8 changes: 6 additions & 2 deletions examples/a_star.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,12 +94,16 @@ fn handle_input(
mut commands: Commands,
buttons: Res<Input<MouseButton>>,
windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<(&Camera, &GlobalTransform)>,
mut current: Local<Hex>,
mut grid: ResMut<HexGrid>,
) {
let window = windows.single();
if let Some(pos) = window.cursor_position() {
let pos = Vec2::new(pos.x - window.width() / 2.0, window.height() / 2.0 - pos.y);
let (camera, cam_transform) = cameras.single();
if let Some(pos) = window
.cursor_position()
.and_then(|p| camera.viewport_to_world_2d(cam_transform, p))
{
let hex_pos = grid.layout.world_pos_to_hex(pos);
let Some(entity) = grid.entities.get(&hex_pos).copied() else { return };
if buttons.just_pressed(MouseButton::Left) {
Expand Down
8 changes: 6 additions & 2 deletions examples/field_of_movement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,17 @@ fn setup_camera(mut commands: Commands) {
/// Input interaction
fn handle_input(
windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<(&Camera, &GlobalTransform)>,
mut tile_transforms: Query<(Entity, &mut Transform)>,
mut current: Local<Hex>,
mut grid: ResMut<HexGrid>,
) {
let window = windows.single();
if let Some(pos) = window.cursor_position() {
let pos = Vec2::new(pos.x - window.width() / 2.0, window.height() / 2.0 - pos.y);
let (camera, cam_transform) = cameras.single();
if let Some(pos) = window
.cursor_position()
.and_then(|p| camera.viewport_to_world_2d(cam_transform, p))
{
let hex_pos = grid.layout.world_pos_to_hex(pos);

if hex_pos == *current {
Expand Down
8 changes: 6 additions & 2 deletions examples/field_of_view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,16 @@ fn handle_input(
mut commands: Commands,
buttons: Res<Input<MouseButton>>,
windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<(&Camera, &GlobalTransform)>,
mut current: Local<Hex>,
mut grid: ResMut<HexGrid>,
) {
let window = windows.single();
if let Some(pos) = window.cursor_position() {
let pos = Vec2::new(pos.x - window.width() / 2.0, window.height() / 2.0 - pos.y);
let (camera, cam_transform) = cameras.single();
if let Some(pos) = window
.cursor_position()
.and_then(|p| camera.viewport_to_world_2d(cam_transform, p))
{
let hex_pos = grid.layout.world_pos_to_hex(pos);
let Some(entity) = grid.entities.get(&hex_pos).copied() else { return };
if buttons.just_pressed(MouseButton::Left) {
Expand Down
8 changes: 6 additions & 2 deletions examples/hex_grid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,16 @@ fn setup_grid(
fn handle_input(
mut commands: Commands,
windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<(&Camera, &GlobalTransform)>,
map: Res<Map>,
mut highlighted_hexes: Local<HighlightedHexes>,
) {
let window = windows.single();
if let Some(pos) = window.cursor_position() {
let pos = Vec2::new(pos.x - window.width() / 2.0, window.height() / 2.0 - pos.y);
let (camera, cam_transform) = cameras.single();
if let Some(pos) = window
.cursor_position()
.and_then(|p| camera.viewport_to_world_2d(cam_transform, p))
{
let coord = map.layout.world_pos_to_hex(pos);
if let Some(entity) = map.entities.get(&coord).copied() {
if coord == highlighted_hexes.selected {
Expand Down
8 changes: 6 additions & 2 deletions examples/scroll_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,15 @@ fn setup_grid(
fn handle_input(
mut commands: Commands,
windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<(&Camera, &GlobalTransform)>,
grid: Res<HexGrid>,
) {
let window = windows.single();
if let Some(pos) = window.cursor_position() {
let pos = Vec2::new(pos.x - window.width() / 2.0, window.height() / 2.0 - pos.y);
let (camera, cam_transform) = cameras.single();
if let Some(pos) = window
.cursor_position()
.and_then(|p| camera.viewport_to_world_2d(cam_transform, p))
{
let hex_pos = grid.layout.world_pos_to_hex(pos);
for h in hex_pos.range(grid.bounds.radius) {
let wrapped = grid.bounds.wrap(h);
Expand Down
8 changes: 6 additions & 2 deletions examples/wrap_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,16 @@ fn setup_grid(
fn handle_input(
mut commands: Commands,
windows: Query<&Window, With<PrimaryWindow>>,
cameras: Query<(&Camera, &GlobalTransform)>,
grid: Res<HexGrid>,
mut current_hex: Local<Hex>,
) {
let window = windows.single();
if let Some(pos) = window.cursor_position() {
let pos = Vec2::new(pos.x - window.width() / 2.0, window.height() / 2.0 - pos.y);
let (camera, cam_transform) = cameras.single();
if let Some(pos) = window
.cursor_position()
.and_then(|p| camera.viewport_to_world_2d(cam_transform, p))
{
let hex_pos = grid.layout.world_pos_to_hex(pos);
if hex_pos == *current_hex {
return;
Expand Down
5 changes: 5 additions & 0 deletions src/direction/hex_direction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ use crate::{DiagonalDirection, HexOrientation};

/// All 6 possible directions in hexagonal space.
///
/// The naming of the variants is based on the standard orientation of both axis
/// but you can invert them in your [`HexLayout`]
///
/// ```txt
/// x Axis
/// ___
Expand Down Expand Up @@ -41,6 +44,8 @@ use crate::{DiagonalDirection, HexOrientation};
/// assert_eq!(direction >> 1, Direction::TopRight);
/// assert_eq!(direction << 1, Direction::TopLeft);
/// ```
///
/// [`HexLayout`]: crate::HexLayout
#[repr(u8)]
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
Expand Down
10 changes: 5 additions & 5 deletions src/hex/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ impl From<[i32; 2]> for Hex {

impl From<(f32, f32)> for Hex {
#[inline]
fn from(v: (f32, f32)) -> Self {
Self::round(v)
fn from((a, b): (f32, f32)) -> Self {
Self::round([a, b])
}
}

impl From<[f32; 2]> for Hex {
#[inline]
fn from([x, y]: [f32; 2]) -> Self {
Self::round((x, y))
fn from(v: [f32; 2]) -> Self {
Self::round(v)
}
}

Expand All @@ -39,7 +39,7 @@ impl From<Hex> for IVec2 {
impl From<Vec2> for Hex {
#[inline]
fn from(value: Vec2) -> Self {
Self::round((value.x, value.y))
Self::from(value.to_array())
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/hex/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ impl Mul<f32> for Hex {
#[inline]
#[allow(clippy::cast_precision_loss)]
fn mul(self, rhs: f32) -> Self::Output {
Self::round((self.x as f32 * rhs, self.y as f32 * rhs))
Self::round([self.x as f32 * rhs, self.y as f32 * rhs])
}
}

Expand Down
14 changes: 7 additions & 7 deletions src/hex/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,9 @@ impl Hex {
pub const Y: Self = Self::new(0, 1);
/// -Y (-R) axis (0, -1)
pub const NEG_Y: Self = Self::new(0, -1);
/// Z (S) axis (0, -1, **1**)
pub const Z: Self = Self::new(0, -1);
/// -Z (S) axis (0, 1, **-1**)
/// Arbitrary cubic Z (S) axis (0, -1, **1**)
pub const Z: Self = Self::NEG_Y;
/// Arbitrary cubic -Z (S) axis (0, 1, **-1**)
pub const NEG_Z: Self = Self::Y;

/// The unit axes.
Expand Down Expand Up @@ -402,13 +402,13 @@ impl Hex {
///
/// ```rust
/// # use hexx::*;
/// let [x, y] = [0.6, 10.2];
/// let coord = Hex::round((x, y));
/// let point = [0.6, 10.2];
/// let coord = Hex::round(point);
/// assert_eq!(coord.x, 1);
/// assert_eq!(coord.y, 10);
/// ```
pub fn round((mut x, mut y): (f32, f32)) -> Self {
let (mut x_r, mut y_r) = (x.round(), y.round());
pub fn round([mut x, mut y]: [f32; 2]) -> Self {
let [mut x_r, mut y_r] = [x.round(), y.round()];
x -= x_r;
y -= y_r;
if x.abs() >= y.abs() {
Expand Down
35 changes: 31 additions & 4 deletions src/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ use glam::Vec2;
/// Hexagonal layout. This type is the bridge between your *world*/*pixel* coordinate system
/// and the hexagonal coordinate system.
///
/// # Axis
///
/// By default, the [`Hex`] `y` axis is pointing down and the `x` axis is pointing right
/// but you have the option to invert them using `invert_x` and `invert_y`
/// This may be useful depending on the coordinate system of your display.
///
/// # Example
///
/// ```rust
Expand All @@ -15,7 +21,11 @@ use glam::Vec2;
/// // We define the world space origin equivalent of `Hex::ZERO` in hex space
/// origin: Vec2::new(1.0, 2.0),
/// // We define the world space size of the hexagons
/// hex_size: Vec2::new(1.0, 1.0)
/// hex_size: Vec2::new(1.0, 1.0),
/// // We invert the y axis
/// invert_y: true,
/// // But not the x axis
/// invert_x: false
/// };
/// // You can now find the world positon (center) of any given hexagon
/// let world_pos = layout.hex_to_world_pos(Hex::ZERO);
Expand All @@ -32,6 +42,10 @@ pub struct HexLayout {
pub origin: Vec2,
/// The size of individual hexagons in world/pixel space. The size can be irregular
pub hex_size: Vec2,
/// If set to `true`, the `Hex` `x` axis will be inverted
pub invert_x: bool,
/// If set to `true`, the `Hex` `y` axis will be inverted
pub invert_y: bool,
}

impl HexLayout {
Expand All @@ -44,6 +58,7 @@ impl HexLayout {
matrix[0].mul_add(hex.x() as f32, matrix[1] * hex.y() as f32),
matrix[2].mul_add(hex.x() as f32, matrix[3] * hex.y() as f32),
) * self.hex_size
* self.axis_scale()
+ self.origin
}

Expand All @@ -52,11 +67,11 @@ impl HexLayout {
/// Computes world/pixel coordinates `pos` into hexagonal coordinates
pub fn world_pos_to_hex(&self, pos: Vec2) -> Hex {
let matrix = self.orientation.inverse_matrix;
let point = (pos - self.origin) / self.hex_size;
Hex::round((
let point = (pos - self.origin) * self.axis_scale() / self.hex_size;
Hex::round([
matrix[0].mul_add(point.x, matrix[1] * point.y),
matrix[2].mul_add(point.x, matrix[3] * point.y),
))
])
}

#[allow(clippy::cast_precision_loss)]
Expand All @@ -69,6 +84,14 @@ impl HexLayout {
center + Vec2::new(self.hex_size.x * angle.cos(), self.hex_size.y * angle.sin())
})
}

#[inline]
/// Returns a signum axis coefficient, allowing for inverted axis
const fn axis_scale(&self) -> Vec2 {
let x = if self.invert_x { -1.0 } else { 1.0 };
let y = if self.invert_y { 1.0 } else { -1.0 };
Vec2::new(x, y)
}
}

impl Default for HexLayout {
Expand All @@ -77,6 +100,8 @@ impl Default for HexLayout {
orientation: HexOrientation::default(),
origin: Vec2::ZERO,
hex_size: Vec2::ONE,
invert_x: false,
invert_y: false,
}
}
}
Expand All @@ -92,6 +117,7 @@ mod tests {
orientation: HexOrientation::Flat,
origin: Vec2::ZERO,
hex_size: Vec2::new(10., 10.),
..Default::default()
};
let corners = layout.hex_corners(point).map(Vec2::round);
assert_eq!(
Expand All @@ -114,6 +140,7 @@ mod tests {
orientation: HexOrientation::Pointy,
origin: Vec2::ZERO,
hex_size: Vec2::new(10., 10.),
..Default::default()
};
let corners = layout.hex_corners(point).map(Vec2::round);
assert_eq!(
Expand Down
2 changes: 2 additions & 0 deletions src/orientation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ static FLAT_ORIENTATION: HexOrientationData = HexOrientationData {
///
/// This struct stored a forward and inverse matrix, for pixel/hex conversion and an angle offset
///
/// See [this article](https://www.redblobgames.com/grids/hexagons/#hex-to-pixel-axial) for more information
///
/// # Usage
///
/// ```rust
Expand Down

0 comments on commit a88a6ec

Please sign in to comment.