feat(citygen): geometry primitives and Grid
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
318
crates/citygen/src/geom.rs
Normal file
318
crates/citygen/src/geom.rs
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
//! Geometry primitives: positions, sizes, rectangles, directions and a dense grid.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::ops::{Index, IndexMut};
|
||||||
|
|
||||||
|
/// A tile coordinate. Negative values are legal and simply out of bounds.
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub struct Pos {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Pos {
|
||||||
|
pub const fn new(x: i32, y: i32) -> Self {
|
||||||
|
Self { x, y }
|
||||||
|
}
|
||||||
|
pub fn offset(self, dx: i32, dy: i32) -> Self {
|
||||||
|
Self::new(self.x + dx, self.y + dy)
|
||||||
|
}
|
||||||
|
pub fn step(self, d: Dir) -> Self {
|
||||||
|
let (dx, dy) = d.delta();
|
||||||
|
self.offset(dx, dy)
|
||||||
|
}
|
||||||
|
pub fn manhattan(self, o: Pos) -> i32 {
|
||||||
|
(self.x - o.x).abs() + (self.y - o.y).abs()
|
||||||
|
}
|
||||||
|
pub fn chebyshev(self, o: Pos) -> i32 {
|
||||||
|
(self.x - o.x).abs().max((self.y - o.y).abs())
|
||||||
|
}
|
||||||
|
pub fn dist2(self, o: Pos) -> i64 {
|
||||||
|
let dx = (self.x - o.x) as i64;
|
||||||
|
let dy = (self.y - o.y) as i64;
|
||||||
|
dx * dx + dy * dy
|
||||||
|
}
|
||||||
|
pub fn neighbors4(self) -> impl Iterator<Item = Pos> {
|
||||||
|
Dir::CARDINAL.into_iter().map(move |d| self.step(d))
|
||||||
|
}
|
||||||
|
pub fn neighbors8(self) -> impl Iterator<Item = Pos> {
|
||||||
|
Dir::ALL.into_iter().map(move |d| self.step(d))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dimensions of a grid.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Size {
|
||||||
|
pub w: u32,
|
||||||
|
pub h: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Size {
|
||||||
|
pub fn area(self) -> usize {
|
||||||
|
self.w as usize * self.h as usize
|
||||||
|
}
|
||||||
|
pub fn contains(self, p: Pos) -> bool {
|
||||||
|
p.x >= 0 && p.y >= 0 && (p.x as u32) < self.w && (p.y as u32) < self.h
|
||||||
|
}
|
||||||
|
pub fn index(self, p: Pos) -> usize {
|
||||||
|
p.y as usize * self.w as usize + p.x as usize
|
||||||
|
}
|
||||||
|
pub fn center(self) -> Pos {
|
||||||
|
Pos::new(self.w as i32 / 2, self.h as i32 / 2)
|
||||||
|
}
|
||||||
|
pub fn rect(self) -> Rect {
|
||||||
|
Rect::new(0, 0, self.w as i32, self.h as i32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Axis-aligned rectangle; `right()` and `bottom()` are exclusive.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub struct Rect {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub w: i32,
|
||||||
|
pub h: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rect {
|
||||||
|
pub const fn new(x: i32, y: i32, w: i32, h: i32) -> Self {
|
||||||
|
Self {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
w: if w < 0 { 0 } else { w },
|
||||||
|
h: if h < 0 { 0 } else { h },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn right(self) -> i32 {
|
||||||
|
self.x + self.w
|
||||||
|
}
|
||||||
|
pub fn bottom(self) -> i32 {
|
||||||
|
self.y + self.h
|
||||||
|
}
|
||||||
|
pub fn is_empty(self) -> bool {
|
||||||
|
self.w == 0 || self.h == 0
|
||||||
|
}
|
||||||
|
pub fn area(self) -> usize {
|
||||||
|
self.w as usize * self.h as usize
|
||||||
|
}
|
||||||
|
pub fn contains(self, p: Pos) -> bool {
|
||||||
|
p.x >= self.x && p.y >= self.y && p.x < self.right() && p.y < self.bottom()
|
||||||
|
}
|
||||||
|
pub fn intersects(self, o: Rect) -> bool {
|
||||||
|
self.x < o.right() && o.x < self.right() && self.y < o.bottom() && o.y < self.bottom()
|
||||||
|
}
|
||||||
|
pub fn intersection(self, o: Rect) -> Option<Rect> {
|
||||||
|
let x = self.x.max(o.x);
|
||||||
|
let y = self.y.max(o.y);
|
||||||
|
let r = self.right().min(o.right());
|
||||||
|
let b = self.bottom().min(o.bottom());
|
||||||
|
if r > x && b > y {
|
||||||
|
Some(Rect::new(x, y, r - x, b - y))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn clamp_to(self, bounds: Rect) -> Option<Rect> {
|
||||||
|
self.intersection(bounds)
|
||||||
|
}
|
||||||
|
pub fn inset(self, n: i32) -> Rect {
|
||||||
|
Rect::new(self.x + n, self.y + n, self.w - 2 * n, self.h - 2 * n)
|
||||||
|
}
|
||||||
|
/// Grow by `n` on every side.
|
||||||
|
pub fn grow(self, n: i32) -> Rect {
|
||||||
|
self.inset(-n)
|
||||||
|
}
|
||||||
|
pub fn center(self) -> Pos {
|
||||||
|
Pos::new(self.x + self.w / 2, self.y + self.h / 2)
|
||||||
|
}
|
||||||
|
pub fn iter(self) -> impl Iterator<Item = Pos> {
|
||||||
|
(self.y..self.bottom())
|
||||||
|
.flat_map(move |y| (self.x..self.right()).map(move |x| Pos::new(x, y)))
|
||||||
|
}
|
||||||
|
pub fn border(self) -> impl Iterator<Item = Pos> {
|
||||||
|
self.iter().filter(move |p| {
|
||||||
|
p.x == self.x || p.y == self.y || p.x == self.right() - 1 || p.y == self.bottom() - 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The eight compass directions.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||||
|
pub enum Dir {
|
||||||
|
N,
|
||||||
|
NE,
|
||||||
|
E,
|
||||||
|
SE,
|
||||||
|
S,
|
||||||
|
SW,
|
||||||
|
W,
|
||||||
|
NW,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Dir {
|
||||||
|
pub const CARDINAL: [Dir; 4] = [Dir::N, Dir::E, Dir::S, Dir::W];
|
||||||
|
pub const ALL: [Dir; 8] = [
|
||||||
|
Dir::N,
|
||||||
|
Dir::NE,
|
||||||
|
Dir::E,
|
||||||
|
Dir::SE,
|
||||||
|
Dir::S,
|
||||||
|
Dir::SW,
|
||||||
|
Dir::W,
|
||||||
|
Dir::NW,
|
||||||
|
];
|
||||||
|
pub fn delta(self) -> (i32, i32) {
|
||||||
|
match self {
|
||||||
|
Dir::N => (0, -1),
|
||||||
|
Dir::NE => (1, -1),
|
||||||
|
Dir::E => (1, 0),
|
||||||
|
Dir::SE => (1, 1),
|
||||||
|
Dir::S => (0, 1),
|
||||||
|
Dir::SW => (-1, 1),
|
||||||
|
Dir::W => (-1, 0),
|
||||||
|
Dir::NW => (-1, -1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn opposite(self) -> Dir {
|
||||||
|
match self {
|
||||||
|
Dir::N => Dir::S,
|
||||||
|
Dir::NE => Dir::SW,
|
||||||
|
Dir::E => Dir::W,
|
||||||
|
Dir::SE => Dir::NW,
|
||||||
|
Dir::S => Dir::N,
|
||||||
|
Dir::SW => Dir::NE,
|
||||||
|
Dir::W => Dir::E,
|
||||||
|
Dir::NW => Dir::SE,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dense row-major grid of `T`.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Grid<T> {
|
||||||
|
size: Size,
|
||||||
|
data: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Grid<T> {
|
||||||
|
pub fn filled(size: Size, v: T) -> Self {
|
||||||
|
Self {
|
||||||
|
size,
|
||||||
|
data: vec![v; size.area()],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn size(&self) -> Size {
|
||||||
|
self.size
|
||||||
|
}
|
||||||
|
pub fn in_bounds(&self, p: Pos) -> bool {
|
||||||
|
self.size.contains(p)
|
||||||
|
}
|
||||||
|
pub fn get(&self, p: Pos) -> Option<&T> {
|
||||||
|
self.in_bounds(p).then(|| &self.data[self.size.index(p)])
|
||||||
|
}
|
||||||
|
pub fn get_mut(&mut self, p: Pos) -> Option<&mut T> {
|
||||||
|
if self.in_bounds(p) {
|
||||||
|
let i = self.size.index(p);
|
||||||
|
Some(&mut self.data[i])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/// Returns false when `p` is out of bounds.
|
||||||
|
pub fn set(&mut self, p: Pos, v: T) -> bool {
|
||||||
|
match self.get_mut(p) {
|
||||||
|
Some(slot) => {
|
||||||
|
*slot = v;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn iter(&self) -> impl Iterator<Item = (Pos, &T)> {
|
||||||
|
let w = self.size.w as i32;
|
||||||
|
self.data
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(move |(i, t)| (Pos::new(i as i32 % w, i as i32 / w), t))
|
||||||
|
}
|
||||||
|
pub fn fill_rect(&mut self, r: Rect, v: T) {
|
||||||
|
for p in r.iter() {
|
||||||
|
self.set(p, v.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn as_slice(&self) -> &[T] {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
pub fn as_mut_slice(&mut self) -> &mut [T] {
|
||||||
|
&mut self.data
|
||||||
|
}
|
||||||
|
pub fn map<U: Clone>(&self, f: impl Fn(&T) -> U) -> Grid<U> {
|
||||||
|
Grid {
|
||||||
|
size: self.size,
|
||||||
|
data: self.data.iter().map(f).collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn count(&self, pred: impl Fn(&T) -> bool) -> usize {
|
||||||
|
self.data.iter().filter(|t| pred(t)).count()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> Index<Pos> for Grid<T> {
|
||||||
|
type Output = T;
|
||||||
|
fn index(&self, p: Pos) -> &T {
|
||||||
|
self.get(p).expect("grid index out of bounds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Clone> IndexMut<Pos> for Grid<T> {
|
||||||
|
fn index_mut(&mut self, p: Pos) -> &mut T {
|
||||||
|
self.get_mut(p).expect("grid index out of bounds")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rect_iter_and_border() {
|
||||||
|
let r = Rect::new(2, 3, 3, 2);
|
||||||
|
assert_eq!(r.area(), 6);
|
||||||
|
assert_eq!(r.iter().count(), 6);
|
||||||
|
assert_eq!(r.border().count(), 6);
|
||||||
|
assert!(r.contains(Pos::new(4, 4)));
|
||||||
|
assert!(!r.contains(Pos::new(5, 4)));
|
||||||
|
assert_eq!(r.inset(1), Rect::new(3, 4, 1, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rect_intersection_and_clamp() {
|
||||||
|
let a = Rect::new(0, 0, 10, 10);
|
||||||
|
let b = Rect::new(5, 5, 10, 10);
|
||||||
|
assert_eq!(a.intersection(b), Some(Rect::new(5, 5, 5, 5)));
|
||||||
|
assert_eq!(a.intersection(Rect::new(20, 20, 1, 1)), None);
|
||||||
|
assert_eq!(b.clamp_to(a), Some(Rect::new(5, 5, 5, 5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grid_set_get_oob() {
|
||||||
|
let mut g = Grid::filled(Size { w: 4, h: 3 }, 0u8);
|
||||||
|
assert!(g.set(Pos::new(3, 2), 7));
|
||||||
|
assert!(!g.set(Pos::new(4, 2), 7));
|
||||||
|
assert_eq!(g.get(Pos::new(3, 2)), Some(&7));
|
||||||
|
assert_eq!(g.get(Pos::new(-1, 0)), None);
|
||||||
|
assert_eq!(g.iter().count(), 12);
|
||||||
|
g.fill_rect(Rect::new(0, 0, 2, 2), 9);
|
||||||
|
assert_eq!(g.count(|&v| v == 9), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dir_delta_opposite() {
|
||||||
|
assert_eq!(Dir::N.delta(), (0, -1));
|
||||||
|
assert_eq!(Dir::SE.opposite(), Dir::NW);
|
||||||
|
assert_eq!(Pos::new(1, 1).step(Dir::W), Pos::new(0, 1));
|
||||||
|
assert_eq!(Pos::new(0, 0).chebyshev(Pos::new(3, -2)), 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
crates/citygen/src/ids.rs
Normal file
23
crates/citygen/src/ids.rs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//! Newtype identifiers for world entities.
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
macro_rules! id_type {
|
||||||
|
($name:ident) => {
|
||||||
|
#[derive(
|
||||||
|
Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
|
||||||
|
)]
|
||||||
|
pub struct $name(pub u32);
|
||||||
|
|
||||||
|
impl $name {
|
||||||
|
pub fn index(self) -> usize {
|
||||||
|
self.0 as usize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
id_type!(DistrictId);
|
||||||
|
id_type!(BuildingId);
|
||||||
|
id_type!(SubnetId);
|
||||||
|
id_type!(NodeId);
|
||||||
@@ -1 +1,7 @@
|
|||||||
//! World model and generator for the cyberpunk city.
|
//! World model and generator for the cyberpunk city.
|
||||||
|
|
||||||
|
pub mod geom;
|
||||||
|
pub mod ids;
|
||||||
|
|
||||||
|
pub use geom::{Dir, Grid, Pos, Rect, Size};
|
||||||
|
pub use ids::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user