Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cargo/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[build]
jobs = 2
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ rand = "0.8"
serde_json = { version = "1.0" }
tiled = { version = "0.14.0", default-features = false }
thiserror = { version = "2.0" }
approx = { version = "0.5" }
proptest = "1.7"
glam = "0.30"

[dev-dependencies.bevy]
version = "0.16.0"
Expand Down
7 changes: 2 additions & 5 deletions examples/helpers/ldtk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use bevy_ecs_tilemap::{
map::{TilemapId, TilemapSize, TilemapTexture, TilemapTileSize},
tiles::{TileBundle, TilePos, TileStorage, TileTextureIndex},
};
use std::{collections::HashMap, io::ErrorKind};
use std::collections::HashMap;
use thiserror::Error;

use bevy::{asset::io::Reader, reflect::TypePath};
Expand Down Expand Up @@ -71,10 +71,7 @@ impl AssetLoader for LdtkLoader {
reader.read_to_end(&mut bytes).await?;

let project: ldtk_rust::Project = serde_json::from_slice(&bytes).map_err(|e| {
std::io::Error::new(
ErrorKind::Other,
format!("Could not read contents of Ldtk map: {e}"),
)
std::io::Error::other(format!("Could not read contents of Ldtk map: {e}"))
})?;
let dependencies: Vec<(i64, AssetPath)> = project
.defs
Expand Down
8 changes: 4 additions & 4 deletions examples/helpers/tiled.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
// * When the 'atlas' feature is enabled tilesets using a collection of images will be skipped.
// * Only finite tile layers are loaded. Infinite tile layers and object layers will be skipped.

use std::io::{Cursor, ErrorKind};
use std::io::Cursor;
use std::path::Path;
use std::sync::Arc;

Expand Down Expand Up @@ -118,9 +118,9 @@ impl AssetLoader for TiledLoader {
tiled::DefaultResourceCache::new(),
BytesResourceReader::new(&bytes),
);
let map = loader.load_tmx_map(load_context.path()).map_err(|e| {
std::io::Error::new(ErrorKind::Other, format!("Could not load TMX map: {e}"))
})?;
let map = loader
.load_tmx_map(load_context.path())
.map_err(|e| std::io::Error::other(format!("Could not load TMX map: {e}")))?;

let mut tilemap_textures = HashMap::default();
#[cfg(not(feature = "atlas"))]
Expand Down
23 changes: 23 additions & 0 deletions mintty.exe.stackdump
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Exception: STATUS_ACCESS_VIOLATION at rip=0010042165E
rax=0000000000000001 rbx=00000000FFFFC1B0 rcx=0000000000000000
rdx=00000000FFFFCE00 rsi=0000000000000000 rdi=000000000000008F
r8 =000000000000005A r9 =000000000000005E r10=0000000100000000
r11=00000001004216A8 r12=0000000000000002 r13=00000000FFFFC1B0
r14=000000080010E650 r15=0000000000000000
rbp=00000000FFFFC270 rsp=00000000FFFFC150
program=C:\Users\Alex Work\AppData\Local\Programs\Git\usr\bin\mintty.exe, pid 905, thread main
cs=0033 ds=002B es=002B fs=0053 gs=002B ss=002B
Stack trace:
Frame Function Args
000FFFFC270 0010042165E (000FFFFC1E4, 00000000000, 00100426F2C, 00000000000)
000FFFFC270 00100422355 (00100427609, 6D6178656D305B1B, 7865745C73656C70, 008000A52A0)
000FFFFC270 0010041A6C4 (001004031A3, FFFFFFFF00000000, 00000000000, 001004E8600)
00000022510 0010042967A (00000000000, 00000000CE6, 001004E8600, 001004E8600)
00000022510 0010042BD70 (001004E300A, 001004E8F40, 00000000018, 000FFFFC3C0)
00000022510 0010042E238 (001004E30E0, 00000000100, 7FF886D6E684, 00000000000)
00000001000 00100404C33 (000FFFFC550, 008000434A0, 7FF886D69680, 00800000001)
000FFFFC550 00100460CB1 (00000000014, 00800042DD8, 000FFFFCB80, 000FFFFCC93)
000FFFFCCE0 0018004B0FB (00000000000, 00000000000, 00000000000, 00000000000)
000FFFFCDA0 00180048A2A (00000000000, 00000000000, 00000000000, 00000000000)
000FFFFCE50 00180048AEC (00000000000, 00000000000, 00000000000, 00000000000)
End of stack trace
50 changes: 50 additions & 0 deletions src/anchor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,53 @@ impl TilemapAnchor {
}
}
}

#[cfg(test)]
mod tests {
use super::*;

fn fixed_params() -> (TilemapSize, TilemapGridSize, TilemapTileSize, TilemapType) {
(
TilemapSize { x: 4, y: 3 },
TilemapGridSize { x: 1.0, y: 1.0 },
TilemapTileSize { x: 1.0, y: 1.0 },
TilemapType::Square,
)
}

#[test]
fn none_anchor_is_zero() {
let (map, grid, tile, ty) = fixed_params();
assert_eq!(
TilemapAnchor::None.as_offset(&map, &grid, &tile, &ty),
Vec2::ZERO
);
}

#[test]
fn center_equals_custom_zero() {
let (map, grid, tile, ty) = fixed_params();
assert_eq!(
TilemapAnchor::Center.as_offset(&map, &grid, &tile, &ty),
TilemapAnchor::Custom(Vec2::ZERO).as_offset(&map, &grid, &tile, &ty)
);
}

#[test]
fn top_left_equals_custom_top_left() {
let (map, grid, tile, ty) = fixed_params();
assert_eq!(
TilemapAnchor::TopLeft.as_offset(&map, &grid, &tile, &ty),
TilemapAnchor::Custom(Vec2::new(-0.5, 0.5)).as_offset(&map, &grid, &tile, &ty)
);
}

#[test]
fn top_right_equals_custom_top_right() {
let (map, grid, tile, ty) = fixed_params();
assert_eq!(
TilemapAnchor::TopRight.as_offset(&map, &grid, &tile, &ty),
TilemapAnchor::Custom(Vec2::new(0.5, 0.5)).as_offset(&map, &grid, &tile, &ty)
);
}
}
28 changes: 28 additions & 0 deletions src/array_texture_preload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,31 @@ pub(crate) fn extract(
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn tilemap_array_texture_default_is_sane() {
// Act
let tex = TilemapArrayTexture::default();

assert!(tex.filter.is_none(), "Filter should start unset (None)");
assert_eq!(
tex.texture,
TilemapTexture::default(),
"Texture default mismatch"
);
assert_eq!(
tex.tile_size,
TilemapTileSize::default(),
"Tile size default mismatch"
);
assert_eq!(
tex.tile_spacing,
TilemapSpacing::default(),
"Tile spacing default mismatch"
);
}
}
59 changes: 54 additions & 5 deletions src/helpers/filling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub fn fill_tilemap(
/// Fills a rectangular region with the given tile.
///
/// The rectangular region is defined by an `origin` in [`TilePos`], and a
/// `size` in tiles ([`TilemapSize`]).
/// `size` in tiles ([`TilemapSize`]).
pub fn fill_tilemap_rect(
texture_index: TileTextureIndex,
origin: TilePos,
Expand Down Expand Up @@ -70,7 +70,7 @@ pub fn fill_tilemap_rect(
/// Fills a rectangular region with colored versions of the given tile.
///
/// The rectangular region is defined by an `origin` in [`TilePos`], and a
/// `size` in tiles ([`TilemapSize`]).
/// `size` in tiles ([`TilemapSize`]).
pub fn fill_tilemap_rect_color(
texture_index: TileTextureIndex,
origin: TilePos,
Expand Down Expand Up @@ -134,8 +134,8 @@ pub fn generate_hex_ring(origin: AxialPos, radius: u32) -> Vec<AxialPos> {
/// Generates a vector of hex positions that form a hexagon of given `radius` around the specified
/// `origin`.
pub fn generate_hexagon(origin: AxialPos, radius: u32) -> Vec<AxialPos> {
let mut hexagon = Vec::with_capacity(1 + (6 * radius * (radius + 1) / 2) as usize);
for r in 0..(radius + 1) {
let mut hexagon = Vec::with_capacity(1 + (((6 * radius * (radius + 1)) / 2) as usize));
for r in 0..radius + 1 {
hexagon.extend(generate_hex_ring(origin, r));
}
hexagon
Expand Down Expand Up @@ -174,7 +174,56 @@ pub fn fill_tilemap_hexagon(
..Default::default()
})
.id();
tile_storage.checked_set(&tile_pos, tile_entity)
tile_storage.checked_set(&tile_pos, tile_entity);
}
});
}

#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;

fn axial_distance(a: AxialPos, b: AxialPos) -> u32 {
let dq = (a.q - b.q).abs() as u32;
let dr = (a.r - b.r).abs() as u32;
let ds = (a.q + a.r - (b.q + b.r)).abs() as u32;
(dq + dr + ds) / 2
}

#[test]
fn ring_radius_zero_is_origin_only() {
let origin = AxialPos::new(0, 0);
let ring = generate_hex_ring(origin, 0);
assert_eq!(ring, vec![origin]);
}

#[test]
fn ring_has_correct_length_and_radius() {
let origin = AxialPos::new(0, 0);
for r in 1..=4 {
let ring = generate_hex_ring(origin, r);
assert_eq!(ring.len() as u32, r * 6, "radius {r}");
// no duplicates & all exactly r away
let uniq: HashSet<_> = ring.iter().cloned().collect();
assert_eq!(uniq.len(), ring.len(), "radius {r} contains duplicates");
assert!(ring.iter().all(|p| axial_distance(*p, origin) == r));
}
}

#[test]
fn hexagon_area_matches_formula_and_contains_rings() {
let origin = AxialPos::new(0, 0);
for r in 0..=4 {
let hex = generate_hexagon(origin, r);
let expected = 1 + 3 * r * (r + 1);
assert_eq!(hex.len() as u32, expected, "radius {r}");
// make sure every inner ring element is inside the hexagon
for r_inner in 0..=r {
for p in generate_hex_ring(origin, r_inner) {
assert!(hex.contains(&p), "ring {r_inner} not fully inside hex {r}");
}
}
}
}
}
4 changes: 3 additions & 1 deletion src/helpers/geometry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use crate::map::TilemapType;
use crate::tiles::TilePos;
use crate::{TilemapAnchor, TilemapGridSize, TilemapSize, TilemapTileSize, Transform};

// Deprecated. Skipping tests.

/// Calculates a [`Transform`] for a tilemap that places it so that its center is at
/// `(0.0, 0.0, z)` in world space.
#[deprecated(since = "0.15.1", note = "please use `TilemapAnchor::Center` instead")]
Expand Down Expand Up @@ -29,5 +31,5 @@ pub fn get_tilemap_center_transform(

let diff = high - low;

Transform::from_xyz(-diff.x / 2., -diff.y / 2., z)
Transform::from_xyz(-diff.x / 2.0, -diff.y / 2.0, z)
}
78 changes: 78 additions & 0 deletions src/helpers/hex_grid/axial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,3 +509,81 @@ impl From<AxialPos> for FractionalAxialPos {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::hex_grid::offset::{ColEvenPos, ColOddPos, RowEvenPos, RowOddPos};

// ---------- small private helper -----------------------------------------------------------
#[test]
fn ceiled_div_by_2_behaves_as_expected() {
// negative inputs
assert_eq!(super::ceiled_division_by_2(-3), -2);
assert_eq!(super::ceiled_division_by_2(-2), -1);
assert_eq!(super::ceiled_division_by_2(-1), -1);

// zero & positives
assert_eq!(super::ceiled_division_by_2(0), 0);
assert_eq!(super::ceiled_division_by_2(1), 1);
assert_eq!(super::ceiled_division_by_2(2), 1);
assert_eq!(super::ceiled_division_by_2(3), 2);
}

// ---------- axial <-> offset round-trips ---------------------------------------------------
const SAMPLE_AXIALS: &[AxialPos] = &[
AxialPos::new(0, 0),
AxialPos::new(2, -1),
AxialPos::new(-4, 5),
AxialPos::new(7, -3),
];

#[test]
fn round_trip_row_odd() {
for &ax in SAMPLE_AXIALS {
let back: AxialPos = RowOddPos::from(ax).into();
assert_eq!(ax, back, "failed on {ax:?}");
}
}

#[test]
fn round_trip_row_even() {
for &ax in SAMPLE_AXIALS {
let back: AxialPos = RowEvenPos::from(ax).into();
assert_eq!(ax, back, "failed on {ax:?}");
}
}

#[test]
fn round_trip_col_odd() {
for &ax in SAMPLE_AXIALS {
let back: AxialPos = ColOddPos::from(ax).into();
assert_eq!(ax, back, "failed on {ax:?}");
}
}

#[test]
fn round_trip_col_even() {
for &ax in SAMPLE_AXIALS {
let back: AxialPos = ColEvenPos::from(ax).into();
assert_eq!(ax, back, "failed on {ax:?}");
}
}

// ---------- magnitude & distance -----------------------------------------------------------
#[test]
fn magnitude_matches_cube_formula() {
assert_eq!(AxialPos::new(0, 0).magnitude(), 0);
assert_eq!(AxialPos::new(1, 0).magnitude(), 1);
assert_eq!(AxialPos::new(1, -1).magnitude(), 1);
assert_eq!(AxialPos::new(2, -1).magnitude(), 2); // (2,-1,-1) → (2+1+1)/2 = 2
}

#[test]
fn distance_is_symmetric_and_correct() {
let a = AxialPos::new(2, -1);
let b = AxialPos::new(-1, 3);
assert_eq!(a.distance_from(&b), b.distance_from(&a));
assert_eq!(a.distance_from(&b), 4);
}
}
42 changes: 42 additions & 0 deletions src/helpers/hex_grid/cube.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,3 +175,45 @@ impl FractionalCubePos {
CubePos { q, r, s }
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::helpers::hex_grid::axial::AxialPos;

#[test]
fn axial_to_cube_preserves_identity() {
let axial = AxialPos { q: 2, r: -3 };
let cube: CubePos = axial.into();
assert_eq!(cube.q + cube.r + cube.s, 0, "q + r + s must be 0");
}

#[test]
fn cube_add_sub_behave_like_vectors() {
let a = CubePos::new(1, -2, 1);
let b = CubePos::new(2, 1, -3);

assert_eq!(a + b, CubePos::new(3, -1, -2));
assert_eq!(a - b, CubePos::new(-1, -3, 4));
// Add with reference
assert_eq!(a + &b, CubePos::new(3, -1, -2));
}

#[test]
fn scalar_multiplication_works_for_i32_and_u32() {
let v = CubePos::new(1, -2, 1);
assert_eq!(3_i32 * v, CubePos::new(3, -6, 3));
assert_eq!(3_u32 * v, CubePos::new(3, -6, 3));
}

#[test]
fn fractional_rounding_gives_containing_hex() {
// Point very close to (1,-1,0) but with largest delta on q
let frac = FractionalCubePos::new(1.49, -1.1, -0.39);
assert_eq!(frac.round(), CubePos::new(1, -1, 0));

// Point inside origin hex
let frac2 = FractionalCubePos::new(0.2, -0.1, -0.1);
assert_eq!(frac2.round(), CubePos::new(0, 0, 0));
}
}
Loading
Loading