initial commit
This commit is contained in:
commit
38e6826440
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
.DS_Store
|
5
Cargo.toml
Normal file
5
Cargo.toml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"libretrogd",
|
||||||
|
"examples/*",
|
||||||
|
]
|
0
examples/.keep
Normal file
0
examples/.keep
Normal file
32
libretrogd/Cargo.toml
Normal file
32
libretrogd/Cargo.toml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[package]
|
||||||
|
name = "libretrogd"
|
||||||
|
description = "A 'retro'-like game development library, for funsies."
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Gered King <gered@blarg.ca>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
low_res = []
|
||||||
|
wide = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sdl2 = { version = "0.34.5", features = ["static-link", "bundled", "unsafe_textures" ] }
|
||||||
|
byte-slice-cast = "1.2.1"
|
||||||
|
byteorder = "1.4.3"
|
||||||
|
thiserror = "1.0.30"
|
||||||
|
rand = "0.8.5"
|
||||||
|
num-traits = "0.2.14"
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
claim = "0.5.0"
|
||||||
|
criterion = "0.3.5"
|
||||||
|
anyhow = "1.0.55"
|
||||||
|
tempfile = "3.3.0"
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "bitmap"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "blit"
|
||||||
|
harness = false
|
BIN
libretrogd/assets/vga.fnt
Normal file
BIN
libretrogd/assets/vga.fnt
Normal file
Binary file not shown.
BIN
libretrogd/assets/vga.pal
Normal file
BIN
libretrogd/assets/vga.pal
Normal file
Binary file not shown.
28
libretrogd/benches/bitmap.rs
Normal file
28
libretrogd/benches/bitmap.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
|
use libretrogd::Bitmap;
|
||||||
|
use libretrogd::Palette;
|
||||||
|
use libretrogd::{SCREEN_HEIGHT, SCREEN_WIDTH};
|
||||||
|
|
||||||
|
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||||
|
let mut source = Bitmap::new(SCREEN_WIDTH, SCREEN_HEIGHT).unwrap();
|
||||||
|
let mut dest = vec![0u32; (SCREEN_WIDTH * SCREEN_HEIGHT * 4) as usize].into_boxed_slice();
|
||||||
|
let palette = Palette::new_vga_palette().unwrap();
|
||||||
|
|
||||||
|
c.bench_function("deindex_bitmap_pixels", |b| {
|
||||||
|
b.iter(|| source.copy_as_argb_to(&mut dest, &palette))
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("set_pixel", |b| {
|
||||||
|
b.iter(|| source.set_pixel(black_box(100), black_box(100), black_box(42)))
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("set_pixel_unchecked", |b| {
|
||||||
|
b.iter(|| unsafe {
|
||||||
|
source.set_pixel_unchecked(black_box(100), black_box(100), black_box(42))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, criterion_benchmark);
|
||||||
|
criterion_main!(benches);
|
62
libretrogd/benches/blit.rs
Normal file
62
libretrogd/benches/blit.rs
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
|
use libretrogd::{Bitmap, BlitMethod, Rect};
|
||||||
|
|
||||||
|
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||||
|
let mut framebuffer = Bitmap::new(320, 240).unwrap();
|
||||||
|
let (bmp, _) = Bitmap::load_iff_file(Path::new("./test-assets/test-tiles.lbm")).unwrap();
|
||||||
|
|
||||||
|
let mut solid_bmp = Bitmap::new(16, 16).unwrap();
|
||||||
|
solid_bmp.blit_region(BlitMethod::Solid, &bmp, &Rect::new(16, 16, 16, 16), 0, 0);
|
||||||
|
let mut trans_bmp = Bitmap::new(16, 16).unwrap();
|
||||||
|
trans_bmp.blit_region(BlitMethod::Solid, &bmp, &Rect::new(160, 0, 16, 16), 0, 0);
|
||||||
|
|
||||||
|
c.bench_function("blit_single_checked_solid", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
framebuffer.blit(
|
||||||
|
black_box(BlitMethod::Solid),
|
||||||
|
black_box(&solid_bmp),
|
||||||
|
black_box(100),
|
||||||
|
black_box(100),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("blit_single_unchecked_solid", |b| {
|
||||||
|
b.iter(|| unsafe {
|
||||||
|
framebuffer.blit_unchecked(
|
||||||
|
black_box(BlitMethod::Solid),
|
||||||
|
black_box(&solid_bmp),
|
||||||
|
black_box(100),
|
||||||
|
black_box(100),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("blit_single_checked_transparent", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
framebuffer.blit(
|
||||||
|
black_box(BlitMethod::Transparent(0)),
|
||||||
|
black_box(&trans_bmp),
|
||||||
|
black_box(100),
|
||||||
|
black_box(100),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
c.bench_function("blit_single_unchecked_transparent", |b| {
|
||||||
|
b.iter(|| unsafe {
|
||||||
|
framebuffer.blit_unchecked(
|
||||||
|
black_box(BlitMethod::Transparent(0)),
|
||||||
|
black_box(&trans_bmp),
|
||||||
|
black_box(100),
|
||||||
|
black_box(100),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, criterion_benchmark);
|
||||||
|
criterion_main!(benches);
|
703
libretrogd/src/entities/mod.rs
Normal file
703
libretrogd/src/entities/mod.rs
Normal file
|
@ -0,0 +1,703 @@
|
||||||
|
use std::any::{TypeId};
|
||||||
|
use std::cell::{Ref, RefCell, RefMut};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
|
use crate::utils::AsAny;
|
||||||
|
|
||||||
|
pub type EntityId = usize;
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// alias `Component` to always be `'static` ...
|
||||||
|
pub trait Component: 'static {}
|
||||||
|
impl<T: 'static> Component for T {}
|
||||||
|
|
||||||
|
pub type ComponentStore<T> = RefCell<HashMap<EntityId, T>>;
|
||||||
|
pub type RefComponents<'a, T> = Ref<'a, HashMap<EntityId, T>>;
|
||||||
|
pub type RefMutComponents<'a, T> = RefMut<'a, HashMap<EntityId, T>>;
|
||||||
|
|
||||||
|
pub trait GenericComponentStore: AsAny {
|
||||||
|
fn has(&self, entity: EntityId) -> bool;
|
||||||
|
fn remove(&mut self, entity: EntityId) -> bool;
|
||||||
|
fn clear(&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Component> GenericComponentStore for ComponentStore<T> {
|
||||||
|
#[inline]
|
||||||
|
fn has(&self, entity: EntityId) -> bool {
|
||||||
|
self.borrow().contains_key(&entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn remove(&mut self, entity: EntityId) -> bool {
|
||||||
|
self.get_mut().remove(&entity).is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn clear(&mut self) {
|
||||||
|
self.get_mut().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn as_component_store<T: Component>(
|
||||||
|
collection: &dyn GenericComponentStore,
|
||||||
|
) -> &ComponentStore<T> {
|
||||||
|
collection.as_any().downcast_ref().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn as_component_store_mut<T: Component>(
|
||||||
|
collection: &mut dyn GenericComponentStore,
|
||||||
|
) -> &mut ComponentStore<T> {
|
||||||
|
collection.as_any_mut().downcast_mut().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
pub struct Entities {
|
||||||
|
entities: HashSet<EntityId>,
|
||||||
|
component_stores: HashMap<TypeId, Box<dyn GenericComponentStore>>,
|
||||||
|
next_id: EntityId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Entities {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Entities {
|
||||||
|
entities: HashSet::new(),
|
||||||
|
component_stores: HashMap::new(),
|
||||||
|
next_id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn has_component_store<T: Component>(&self) -> bool {
|
||||||
|
let type_id = TypeId::of::<T>();
|
||||||
|
self.component_stores.contains_key(&type_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_component_store<T: Component>(&self) -> Option<&ComponentStore<T>> {
|
||||||
|
if !self.has_component_store::<T>() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
let type_id = TypeId::of::<T>();
|
||||||
|
Some(as_component_store(
|
||||||
|
self.component_stores.get(&type_id).unwrap().as_ref(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_component_store<T: Component>(&mut self) -> &ComponentStore<T> {
|
||||||
|
if self.has_component_store::<T>() {
|
||||||
|
self.get_component_store().unwrap()
|
||||||
|
} else {
|
||||||
|
let component_store = ComponentStore::<T>::new(HashMap::new());
|
||||||
|
let type_id = TypeId::of::<T>();
|
||||||
|
self.component_stores
|
||||||
|
.insert(type_id, Box::new(component_store));
|
||||||
|
as_component_store(self.component_stores.get(&type_id).unwrap().as_ref())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn has_entity(&self, entity: EntityId) -> bool {
|
||||||
|
self.entities.contains(&entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_entity(&mut self) -> EntityId {
|
||||||
|
let new_entity_id = self.next_id;
|
||||||
|
self.next_id = self.next_id.wrapping_add(1);
|
||||||
|
self.entities.insert(new_entity_id);
|
||||||
|
new_entity_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_entity(&mut self, entity: EntityId) -> bool {
|
||||||
|
if !self.has_entity(entity) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.entities.remove(&entity);
|
||||||
|
for (_, component_store) in self.component_stores.iter_mut() {
|
||||||
|
component_store.remove(entity);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_all_entities(&mut self) {
|
||||||
|
self.entities.clear();
|
||||||
|
for (_, component_store) in self.component_stores.iter_mut() {
|
||||||
|
component_store.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_component<T: Component>(&self, entity: EntityId) -> bool {
|
||||||
|
if !self.has_entity(entity) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
let type_id = TypeId::of::<T>();
|
||||||
|
if let Some(component_store) = self.component_stores.get(&type_id) {
|
||||||
|
component_store.has(entity)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_component<T: Component>(&mut self, entity: EntityId, component: T) -> bool {
|
||||||
|
if !self.has_entity(entity) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
if let Some(component_store) = self.get_component_store::<T>() {
|
||||||
|
component_store.borrow_mut().insert(entity, component);
|
||||||
|
} else {
|
||||||
|
self.add_component_store::<T>()
|
||||||
|
.borrow_mut()
|
||||||
|
.insert(entity, component);
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_component<T: Component>(&mut self, entity: EntityId) -> bool {
|
||||||
|
if !self.has_entity(entity) {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
let type_id = TypeId::of::<T>();
|
||||||
|
if let Some(component_store) = self.component_stores.get_mut(&type_id) {
|
||||||
|
component_store.remove(entity)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn components<T: Component>(&self) -> Option<RefComponents<T>> {
|
||||||
|
if let Some(component_store) = self.get_component_store() {
|
||||||
|
Some(component_store.borrow())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn components_mut<T: Component>(&self) -> Option<RefMutComponents<T>> {
|
||||||
|
if let Some(component_store) = self.get_component_store() {
|
||||||
|
Some(component_store.borrow_mut())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn init_components<T: Component>(&mut self) {
|
||||||
|
if self.get_component_store::<T>().is_none() {
|
||||||
|
self.add_component_store::<T>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// TODO: is there some fancy way to get rid of the impl duplication here ... ?
|
||||||
|
|
||||||
|
pub trait ComponentStoreConvenience<T: Component> {
|
||||||
|
fn single(&self) -> Option<(&EntityId, &T)>;
|
||||||
|
fn get(&self, k: &EntityId) -> Option<&T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ComponentStoreConvenienceMut<T: Component> {
|
||||||
|
fn single_mut(&mut self) -> Option<(&EntityId, &T)>;
|
||||||
|
fn get_mut(&mut self, k: &EntityId) -> Option<&mut T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Component> ComponentStoreConvenience<T> for Option<RefComponents<'a, T>> {
|
||||||
|
fn single(&self) -> Option<(&EntityId, &T)> {
|
||||||
|
if let Some(components) = self {
|
||||||
|
if let Some((entity_id, component)) = components.iter().next() {
|
||||||
|
return Some((entity_id, component));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, k: &EntityId) -> Option<&T> {
|
||||||
|
if let Some(components) = self {
|
||||||
|
components.get(k)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Component> ComponentStoreConvenience<T> for Option<RefMutComponents<'a, T>> {
|
||||||
|
fn single(&self) -> Option<(&EntityId, &T)> {
|
||||||
|
if let Some(components) = self {
|
||||||
|
if let Some((entity_id, component)) = components.iter().next() {
|
||||||
|
return Some((entity_id, component));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, k: &EntityId) -> Option<&T> {
|
||||||
|
if let Some(components) = self {
|
||||||
|
components.get(k)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Component> ComponentStoreConvenienceMut<T> for Option<RefMutComponents<'a, T>> {
|
||||||
|
fn single_mut(&mut self) -> Option<(&EntityId, &T)> {
|
||||||
|
if let Some(components) = self {
|
||||||
|
if let Some((entity_id, component)) = components.iter_mut().next() {
|
||||||
|
return Some((entity_id, component));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_mut(&mut self, k: &EntityId) -> Option<&mut T> {
|
||||||
|
if let Some(components) = self {
|
||||||
|
components.get_mut(k)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait OptionComponentStore<T: Component> {
|
||||||
|
fn len(&self) -> usize;
|
||||||
|
fn is_empty(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Component> OptionComponentStore<T> for Option<RefComponents<'a, T>> {
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
if let Some(components) = self {
|
||||||
|
components.len()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
if let Some(components) = self {
|
||||||
|
components.is_empty()
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T: Component> OptionComponentStore<T> for Option<RefMutComponents<'a, T>> {
|
||||||
|
fn len(&self) -> usize {
|
||||||
|
if let Some(components) = self {
|
||||||
|
components.len()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
if let Some(components) = self {
|
||||||
|
components.is_empty()
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
pub type UpdateFn<T> = fn(&mut Entities, &mut T);
|
||||||
|
pub type RenderFn<T> = fn(&mut Entities, &mut T);
|
||||||
|
|
||||||
|
pub struct ComponentSystems<U, R> {
|
||||||
|
update_systems: Vec<UpdateFn<U>>,
|
||||||
|
render_systems: Vec<RenderFn<R>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<U, R> ComponentSystems<U, R> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
ComponentSystems {
|
||||||
|
update_systems: Vec::new(),
|
||||||
|
render_systems: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_update_system(&mut self, f: UpdateFn<U>) {
|
||||||
|
self.update_systems.push(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_render_system(&mut self, f: RenderFn<R>) {
|
||||||
|
self.render_systems.push(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset(&mut self) {
|
||||||
|
self.update_systems.clear();
|
||||||
|
self.render_systems.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update(&mut self, entities: &mut Entities, context: &mut U) {
|
||||||
|
for f in self.update_systems.iter_mut() {
|
||||||
|
f(entities, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn render(&mut self, entities: &mut Entities, context: &mut R) {
|
||||||
|
for f in self.render_systems.iter_mut() {
|
||||||
|
f(entities, context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use claim::*;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||||
|
struct Name(&'static str);
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||||
|
struct Position(i32, i32);
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||||
|
struct Velocity(i32, i32);
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||||
|
struct Health(u32);
|
||||||
|
#[derive(Debug, Eq, PartialEq, Hash, Clone)]
|
||||||
|
struct Counter(u32);
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_and_remove_entities() {
|
||||||
|
let mut em = Entities::new();
|
||||||
|
|
||||||
|
// add first entity
|
||||||
|
assert!(!em.has_entity(1));
|
||||||
|
let a = em.new_entity();
|
||||||
|
assert!(em.has_entity(a));
|
||||||
|
|
||||||
|
// add second entity with totally different id from first entity
|
||||||
|
let b = em.new_entity();
|
||||||
|
assert!(em.has_entity(a));
|
||||||
|
assert!(em.has_entity(b));
|
||||||
|
assert_ne!(a, b);
|
||||||
|
|
||||||
|
// remove first entity
|
||||||
|
assert!(em.remove_entity(a));
|
||||||
|
assert!(!em.has_entity(a));
|
||||||
|
assert!(em.has_entity(b));
|
||||||
|
|
||||||
|
// remove second entity
|
||||||
|
assert!(em.remove_entity(b));
|
||||||
|
assert!(!em.has_entity(b));
|
||||||
|
|
||||||
|
// removing entities which don't exist shouldn't blow up
|
||||||
|
assert!(!em.remove_entity(a));
|
||||||
|
assert!(!em.remove_entity(b));
|
||||||
|
|
||||||
|
// add third entity, will not re-use previous entity ids
|
||||||
|
let c = em.new_entity();
|
||||||
|
assert!(em.has_entity(c));
|
||||||
|
assert_ne!(a, c);
|
||||||
|
assert_ne!(b, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn add_and_remove_entity_components() {
|
||||||
|
let mut em = Entities::new();
|
||||||
|
|
||||||
|
// create first entity
|
||||||
|
let a = em.new_entity();
|
||||||
|
assert!(em.has_entity(a));
|
||||||
|
|
||||||
|
// add component
|
||||||
|
assert!(!em.has_component::<Name>(a));
|
||||||
|
assert!(em.add_component(a, Name("Someone")));
|
||||||
|
assert!(em.has_component::<Name>(a));
|
||||||
|
|
||||||
|
// verify the added component
|
||||||
|
{
|
||||||
|
let names = em.components::<Name>().unwrap();
|
||||||
|
let name_a = names.get(&a).unwrap();
|
||||||
|
assert_eq!("Someone", name_a.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create second entity
|
||||||
|
let b = em.new_entity();
|
||||||
|
assert!(em.has_entity(b));
|
||||||
|
|
||||||
|
// add component to second entity
|
||||||
|
assert!(!em.has_component::<Position>(b));
|
||||||
|
assert_none!(em.components::<Position>());
|
||||||
|
assert!(em.add_component(b, Position(1, 2)));
|
||||||
|
assert!(em.has_component::<Position>(b));
|
||||||
|
|
||||||
|
// verify the added component
|
||||||
|
{
|
||||||
|
let positions = em.components::<Position>().unwrap();
|
||||||
|
let position_b = positions.get(&b).unwrap();
|
||||||
|
assert!(1 == position_b.0 && 2 == position_b.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify current components for both entities are what we expect
|
||||||
|
assert!(em.has_component::<Name>(a));
|
||||||
|
assert!(!em.has_component::<Name>(b));
|
||||||
|
assert!(!em.has_component::<Position>(a));
|
||||||
|
assert!(em.has_component::<Position>(b));
|
||||||
|
|
||||||
|
// add new component to first entity
|
||||||
|
assert!(em.add_component(a, Position(5, 3)));
|
||||||
|
assert!(em.has_component::<Position>(a));
|
||||||
|
|
||||||
|
// verify both position components for both entities
|
||||||
|
{
|
||||||
|
let positions = em.components::<Position>().unwrap();
|
||||||
|
let position_a = positions.get(&a).unwrap();
|
||||||
|
assert!(5 == position_a.0 && 3 == position_a.1);
|
||||||
|
let position_b = positions.get(&b).unwrap();
|
||||||
|
assert!(1 == position_b.0 && 2 == position_b.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify current components for both entities are what we expect
|
||||||
|
assert!(em.has_component::<Name>(a));
|
||||||
|
assert!(!em.has_component::<Name>(b));
|
||||||
|
assert!(em.has_component::<Position>(a));
|
||||||
|
assert!(em.has_component::<Position>(b));
|
||||||
|
|
||||||
|
// remove position component from first entity
|
||||||
|
assert!(em.remove_component::<Position>(a));
|
||||||
|
assert!(!em.has_component::<Position>(a));
|
||||||
|
|
||||||
|
// verify current components for both entities are what we expect
|
||||||
|
assert!(em.has_component::<Name>(a));
|
||||||
|
assert!(!em.has_component::<Name>(b));
|
||||||
|
assert!(!em.has_component::<Position>(a));
|
||||||
|
assert!(em.has_component::<Position>(b));
|
||||||
|
{
|
||||||
|
let positions = em.components::<Position>().unwrap();
|
||||||
|
let position_b = positions.get(&b).unwrap();
|
||||||
|
assert!(1 == position_b.0 && 2 == position_b.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn modify_components() {
|
||||||
|
let mut em = Entities::new();
|
||||||
|
|
||||||
|
// create entities
|
||||||
|
let a = em.new_entity();
|
||||||
|
em.add_component(a, Position(10, 20));
|
||||||
|
let b = em.new_entity();
|
||||||
|
em.add_component(b, Position(17, 5));
|
||||||
|
|
||||||
|
// change entity positions
|
||||||
|
{
|
||||||
|
let mut positions = em.components_mut::<Position>().unwrap();
|
||||||
|
let position_a = positions.get_mut(&a).unwrap();
|
||||||
|
assert_eq!(Position(10, 20), *position_a);
|
||||||
|
position_a.0 = 13;
|
||||||
|
position_a.1 = 23;
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut positions = em.components_mut::<Position>().unwrap();
|
||||||
|
let position_b = positions.get_mut(&b).unwrap();
|
||||||
|
assert_eq!(Position(17, 5), *position_b);
|
||||||
|
position_b.0 = 15;
|
||||||
|
position_b.1 = 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify both entity position components
|
||||||
|
{
|
||||||
|
let positions = em.components::<Position>().unwrap();
|
||||||
|
let position_a = positions.get(&a).unwrap();
|
||||||
|
assert_eq!(Position(13, 23), *position_a);
|
||||||
|
let position_b = positions.get(&b).unwrap();
|
||||||
|
assert_eq!(Position(15, 8), *position_b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_all_components_of_type() {
|
||||||
|
let mut em = Entities::new();
|
||||||
|
|
||||||
|
// create entities
|
||||||
|
let a = em.new_entity();
|
||||||
|
em.add_component(a, Health(20));
|
||||||
|
em.add_component(a, Position(10, 20));
|
||||||
|
let b = em.new_entity();
|
||||||
|
em.add_component(b, Health(30));
|
||||||
|
em.add_component(b, Position(17, 5));
|
||||||
|
|
||||||
|
// verify initial entity positions
|
||||||
|
{
|
||||||
|
let positions = em.components::<Position>().unwrap();
|
||||||
|
assert_eq!(2, positions.len());
|
||||||
|
let positions: HashSet<&Position> = positions.values().collect();
|
||||||
|
assert!(positions.contains(&Position(10, 20)));
|
||||||
|
assert!(positions.contains(&Position(17, 5)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// modify position components
|
||||||
|
{
|
||||||
|
let mut positions = em.components_mut::<Position>().unwrap();
|
||||||
|
for mut component in positions.values_mut() {
|
||||||
|
component.0 += 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(Position(15, 20), *positions.get(&a).unwrap());
|
||||||
|
assert_eq!(Position(22, 5), *positions.get(&b).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// modify health components
|
||||||
|
{
|
||||||
|
let mut healths = em.components_mut::<Health>().unwrap();
|
||||||
|
for mut component in healths.values_mut() {
|
||||||
|
component.0 += 5;
|
||||||
|
}
|
||||||
|
assert_eq!(Health(25), *healths.get(&a).unwrap());
|
||||||
|
assert_eq!(Health(35), *healths.get(&b).unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_all_entities_with_component() {
|
||||||
|
let mut em = Entities::new();
|
||||||
|
|
||||||
|
// create entities
|
||||||
|
let a = em.new_entity();
|
||||||
|
em.add_component(a, Name("A"));
|
||||||
|
em.add_component(a, Health(20));
|
||||||
|
em.add_component(a, Position(10, 20));
|
||||||
|
let b = em.new_entity();
|
||||||
|
em.add_component(b, Name("B"));
|
||||||
|
em.add_component(b, Position(17, 5));
|
||||||
|
|
||||||
|
// get entities with position components
|
||||||
|
{
|
||||||
|
let positions = em.components::<Position>().unwrap();
|
||||||
|
let entities = positions.keys();
|
||||||
|
assert_eq!(2, entities.len());
|
||||||
|
let entities: HashSet<&EntityId> = entities.collect();
|
||||||
|
assert!(entities.contains(&a));
|
||||||
|
assert!(entities.contains(&b));
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
let names = em.components::<Name>().unwrap();
|
||||||
|
for (entity, name) in names.iter() {
|
||||||
|
// just written this way to verify can grab two mutable components at once
|
||||||
|
// (since this wouldn't be an uncommon way to want to work with an entity)
|
||||||
|
let mut healths = em.components_mut::<Health>().unwrap();
|
||||||
|
let mut positions = em.components_mut::<Position>().unwrap();
|
||||||
|
|
||||||
|
let health = healths.get_mut(&entity);
|
||||||
|
let position = positions.get_mut(&entity);
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"entity {}, health: {:?}, position: {:?}",
|
||||||
|
name.0, health, position
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(mut health) = health {
|
||||||
|
health.0 += 5;
|
||||||
|
}
|
||||||
|
if let Some(mut position) = position {
|
||||||
|
position.0 += 5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let positions = em.components::<Position>().unwrap();
|
||||||
|
assert_eq!(Position(15, 20), *positions.get(&a).unwrap());
|
||||||
|
assert_eq!(Position(22, 5), *positions.get(&b).unwrap());
|
||||||
|
let healths = em.components::<Health>().unwrap();
|
||||||
|
assert_eq!(Health(25), *healths.get(&a).unwrap());
|
||||||
|
assert!(healths.get(&b).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
struct UpdateContext(f32);
|
||||||
|
struct RenderContext(f32);
|
||||||
|
|
||||||
|
fn system_print_entity_positions(entities: &mut Entities, _context: &mut RenderContext) {
|
||||||
|
let positions = entities.components::<Position>().unwrap();
|
||||||
|
for (entity, position) in positions.iter() {
|
||||||
|
println!("entity {} at x:{}, y:{}", entity, position.0, position.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn system_move_entities_forward(entities: &mut Entities, _context: &mut UpdateContext) {
|
||||||
|
let mut positions = entities.components_mut::<Position>().unwrap();
|
||||||
|
let velocities = entities.components::<Velocity>().unwrap();
|
||||||
|
for (entity, position) in positions.iter_mut() {
|
||||||
|
if let Some(velocity) = velocities.get(&entity) {
|
||||||
|
position.0 += velocity.0;
|
||||||
|
position.1 += velocity.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn system_increment_counter(entities: &mut Entities, _context: &mut UpdateContext) {
|
||||||
|
let mut counters = entities.components_mut::<Counter>().unwrap();
|
||||||
|
for (_entity, counter) in counters.iter_mut() {
|
||||||
|
counter.0 += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn system_print_counter(entities: &mut Entities, _context: &mut RenderContext) {
|
||||||
|
let counters = entities.components::<Counter>().unwrap();
|
||||||
|
for (entity, counter) in counters.iter() {
|
||||||
|
println!("entity {} has counter {}", entity, counter.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn component_systems() {
|
||||||
|
let mut em = Entities::new();
|
||||||
|
|
||||||
|
// create entities
|
||||||
|
let a = em.new_entity();
|
||||||
|
em.add_component(a, Position(5, 6));
|
||||||
|
em.add_component(a, Velocity(1, 1));
|
||||||
|
em.add_component(a, Counter(0));
|
||||||
|
let b = em.new_entity();
|
||||||
|
em.add_component(b, Position(-3, 0));
|
||||||
|
em.add_component(b, Velocity(1, 0));
|
||||||
|
em.add_component(b, Counter(0));
|
||||||
|
let c = em.new_entity();
|
||||||
|
em.add_component(c, Position(2, 9));
|
||||||
|
em.add_component(c, Counter(0));
|
||||||
|
|
||||||
|
// setup component systems
|
||||||
|
let mut cs = ComponentSystems::new();
|
||||||
|
cs.add_update_system(system_move_entities_forward);
|
||||||
|
cs.add_update_system(system_increment_counter);
|
||||||
|
cs.add_render_system(system_print_entity_positions);
|
||||||
|
cs.add_render_system(system_print_counter);
|
||||||
|
|
||||||
|
// run some update+render iterations
|
||||||
|
for _ in 0..5 {
|
||||||
|
let mut update_context = UpdateContext(0.0);
|
||||||
|
let mut render_context = RenderContext(0.0);
|
||||||
|
cs.update(&mut em, &mut update_context);
|
||||||
|
cs.render(&mut em, &mut render_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify expected entity positions
|
||||||
|
let positions = em.components::<Position>().unwrap();
|
||||||
|
let velocities = em.components::<Velocity>().unwrap();
|
||||||
|
let counters = em.components::<Counter>().unwrap();
|
||||||
|
assert_eq!(Position(10, 11), *positions.get(&a).unwrap());
|
||||||
|
assert_eq!(Velocity(1, 1), *velocities.get(&a).unwrap());
|
||||||
|
assert_eq!(Counter(5), *counters.get(&a).unwrap());
|
||||||
|
assert_eq!(Position(2, 0), *positions.get(&b).unwrap());
|
||||||
|
assert_eq!(Velocity(1, 0), *velocities.get(&b).unwrap());
|
||||||
|
assert_eq!(Counter(5), *counters.get(&b).unwrap());
|
||||||
|
assert_eq!(Position(2, 9), *positions.get(&c).unwrap());
|
||||||
|
assert_eq!(None, velocities.get(&c));
|
||||||
|
assert_eq!(Counter(5), *counters.get(&c).unwrap());
|
||||||
|
}
|
||||||
|
}
|
291
libretrogd/src/events/mod.rs
Normal file
291
libretrogd/src/events/mod.rs
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
pub type ListenerFn<EventType, ContextType> = fn(event: &EventType, &mut ContextType) -> bool;
|
||||||
|
|
||||||
|
pub struct EventPublisher<EventType> {
|
||||||
|
queue: VecDeque<EventType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<EventType> EventPublisher<EventType> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
EventPublisher {
|
||||||
|
queue: VecDeque::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.queue.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.queue.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn queue(&mut self, event: EventType) {
|
||||||
|
self.queue.push_back(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_queue(&mut self, destination: &mut VecDeque<EventType>) {
|
||||||
|
destination.clear();
|
||||||
|
destination.append(&mut self.queue);
|
||||||
|
self.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct EventListeners<EventType, ContextType> {
|
||||||
|
listeners: Vec<ListenerFn<EventType, ContextType>>,
|
||||||
|
dispatch_queue: VecDeque<EventType>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<EventType, ContextType> EventListeners<EventType, ContextType> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
EventListeners {
|
||||||
|
listeners: Vec::new(),
|
||||||
|
dispatch_queue: VecDeque::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.listeners.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.listeners.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, listener: ListenerFn<EventType, ContextType>) -> bool {
|
||||||
|
// HACK?: most advice i've seen right now for comparing function pointers suggests doing
|
||||||
|
// this, but i've also come across comments suggesting there are times where this
|
||||||
|
// might not be foolproof? (e.g. where generics or lifetimes come into play ... )
|
||||||
|
if self.listeners.iter().any(|&l| l as usize == listener as usize) {
|
||||||
|
false // don't add a duplicate listener
|
||||||
|
} else {
|
||||||
|
self.listeners.push(listener);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, listener: ListenerFn<EventType, ContextType>) -> bool {
|
||||||
|
let before_size = self.listeners.len();
|
||||||
|
// HACK?: comparing function pointers -- see above "HACK?" comment. same concern here.
|
||||||
|
self.listeners.retain(|&l| l as usize != listener as usize);
|
||||||
|
// return true if the listener was removed
|
||||||
|
return before_size != self.listeners.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn take_queue_from(&mut self, publisher: &mut EventPublisher<EventType>) -> usize {
|
||||||
|
publisher.take_queue(&mut self.dispatch_queue);
|
||||||
|
self.dispatch_queue.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dispatch_queue(&mut self, context: &mut ContextType) {
|
||||||
|
while let Some(event) = self.dispatch_queue.pop_front() {
|
||||||
|
for listener in &self.listeners {
|
||||||
|
if listener(&event, context) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
|
||||||
|
enum TestEvent {
|
||||||
|
Dummy,
|
||||||
|
Foobar(i32),
|
||||||
|
Message(&'static str),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DummyContext;
|
||||||
|
|
||||||
|
struct TestContext {
|
||||||
|
pub count: i32,
|
||||||
|
pub events: Vec<TestEvent>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestContext {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
TestContext { count: 0, events: Vec::new() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dummy_listener(_event: &TestEvent, _context: &mut DummyContext) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn other_dummy_listener(_event: &TestEvent, _context: &mut DummyContext) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_logger(event: &TestEvent, context: &mut TestContext) -> bool {
|
||||||
|
context.events.push(*event);
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn event_counter(_event: &TestEvent, context: &mut TestContext) -> bool {
|
||||||
|
context.count += 1;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn message_filter(event: &TestEvent, _context: &mut TestContext) -> bool {
|
||||||
|
match event {
|
||||||
|
TestEvent::Message(s) => {
|
||||||
|
if *s == "filter" {
|
||||||
|
true // means event was handled, and no subsequent listeners should be called
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn adding_and_removing_listeners() {
|
||||||
|
let mut listeners = EventListeners::<TestEvent, DummyContext>::new();
|
||||||
|
|
||||||
|
// add and remove
|
||||||
|
assert_eq!(0, listeners.len());
|
||||||
|
assert!(listeners.add(dummy_listener));
|
||||||
|
assert_eq!(1, listeners.len());
|
||||||
|
assert!(!listeners.add(dummy_listener));
|
||||||
|
assert_eq!(1, listeners.len());
|
||||||
|
assert!(listeners.remove(dummy_listener));
|
||||||
|
assert_eq!(0, listeners.len());
|
||||||
|
assert!(!listeners.remove(dummy_listener));
|
||||||
|
assert_eq!(0, listeners.len());
|
||||||
|
|
||||||
|
// add and remove multiple
|
||||||
|
assert!(listeners.add(dummy_listener));
|
||||||
|
assert_eq!(1, listeners.len());
|
||||||
|
assert!(listeners.add(other_dummy_listener));
|
||||||
|
assert_eq!(2, listeners.len());
|
||||||
|
assert!(listeners.remove(dummy_listener));
|
||||||
|
assert_eq!(1, listeners.len());
|
||||||
|
assert!(!listeners.remove(dummy_listener));
|
||||||
|
assert_eq!(1, listeners.len());
|
||||||
|
assert!(listeners.remove(other_dummy_listener));
|
||||||
|
assert_eq!(0, listeners.len());
|
||||||
|
|
||||||
|
// clear all
|
||||||
|
assert!(listeners.add(dummy_listener));
|
||||||
|
assert!(listeners.add(other_dummy_listener));
|
||||||
|
assert_eq!(2, listeners.len());
|
||||||
|
listeners.clear();
|
||||||
|
assert_eq!(0, listeners.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn queueing_events() {
|
||||||
|
use TestEvent::*;
|
||||||
|
|
||||||
|
let mut publisher = EventPublisher::<TestEvent>::new();
|
||||||
|
assert_eq!(0, publisher.len());
|
||||||
|
publisher.queue(Dummy);
|
||||||
|
assert_eq!(1, publisher.len());
|
||||||
|
publisher.queue(Foobar(1));
|
||||||
|
assert_eq!(2, publisher.len());
|
||||||
|
publisher.queue(Foobar(2));
|
||||||
|
assert_eq!(3, publisher.len());
|
||||||
|
|
||||||
|
let mut queue = VecDeque::<TestEvent>::new();
|
||||||
|
publisher.take_queue(&mut queue);
|
||||||
|
assert_eq!(0, publisher.len());
|
||||||
|
assert_eq!(Dummy, queue.pop_front().unwrap());
|
||||||
|
assert_eq!(Foobar(1), queue.pop_front().unwrap());
|
||||||
|
assert_eq!(Foobar(2), queue.pop_front().unwrap());
|
||||||
|
assert!(queue.pop_front().is_none());
|
||||||
|
|
||||||
|
publisher.queue(Dummy);
|
||||||
|
assert_eq!(1, publisher.len());
|
||||||
|
publisher.clear();
|
||||||
|
assert_eq!(0, publisher.len());
|
||||||
|
let mut queue = VecDeque::<TestEvent>::new();
|
||||||
|
publisher.take_queue(&mut queue);
|
||||||
|
assert_eq!(0, publisher.len());
|
||||||
|
assert_eq!(0, queue.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn listeners_receive_events() {
|
||||||
|
use TestEvent::*;
|
||||||
|
|
||||||
|
let mut listeners = EventListeners::<TestEvent, TestContext>::new();
|
||||||
|
assert!(listeners.add(event_logger));
|
||||||
|
|
||||||
|
let mut publisher = EventPublisher::<TestEvent>::new();
|
||||||
|
publisher.queue(Dummy);
|
||||||
|
publisher.queue(Foobar(1));
|
||||||
|
publisher.queue(Dummy);
|
||||||
|
publisher.queue(Foobar(42));
|
||||||
|
assert_eq!(4, listeners.take_queue_from(&mut publisher));
|
||||||
|
|
||||||
|
let mut context = TestContext::new();
|
||||||
|
assert!(context.events.is_empty());
|
||||||
|
assert_eq!(0, context.count);
|
||||||
|
listeners.dispatch_queue(&mut context);
|
||||||
|
assert!(!context.events.is_empty());
|
||||||
|
assert_eq!(0, context.count);
|
||||||
|
assert_eq!(
|
||||||
|
vec![Dummy, Foobar(1), Dummy, Foobar(42)],
|
||||||
|
context.events
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut context = TestContext::new();
|
||||||
|
assert!(context.events.is_empty());
|
||||||
|
assert_eq!(0, context.count);
|
||||||
|
listeners.dispatch_queue(&mut context);
|
||||||
|
assert!(context.events.is_empty());
|
||||||
|
|
||||||
|
assert!(listeners.add(event_counter));
|
||||||
|
publisher.queue(Foobar(10));
|
||||||
|
publisher.queue(Foobar(20));
|
||||||
|
publisher.queue(Dummy);
|
||||||
|
listeners.take_queue_from(&mut publisher);
|
||||||
|
let mut context = TestContext::new();
|
||||||
|
listeners.dispatch_queue(&mut context);
|
||||||
|
assert!(!context.events.is_empty());
|
||||||
|
assert_eq!(3, context.count);
|
||||||
|
assert_eq!(
|
||||||
|
vec![Foobar(10), Foobar(20), Dummy],
|
||||||
|
context.events
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn listener_filtering() {
|
||||||
|
use TestEvent::*;
|
||||||
|
|
||||||
|
let mut listeners = EventListeners::<TestEvent, TestContext>::new();
|
||||||
|
assert!(listeners.add(message_filter));
|
||||||
|
assert!(listeners.add(event_logger));
|
||||||
|
assert!(listeners.add(event_counter));
|
||||||
|
|
||||||
|
let mut publisher = EventPublisher::<TestEvent>::new();
|
||||||
|
publisher.queue(Message("hello"));
|
||||||
|
publisher.queue(Dummy);
|
||||||
|
publisher.queue(Message("filter"));
|
||||||
|
publisher.queue(Foobar(3));
|
||||||
|
assert_eq!(4, listeners.take_queue_from(&mut publisher));
|
||||||
|
|
||||||
|
let mut context = TestContext::new();
|
||||||
|
assert!(context.events.is_empty());
|
||||||
|
assert_eq!(0, context.count);
|
||||||
|
listeners.dispatch_queue(&mut context);
|
||||||
|
assert!(!context.events.is_empty());
|
||||||
|
assert_eq!(3, context.count);
|
||||||
|
assert_eq!(
|
||||||
|
vec![Message("hello"), Dummy, Foobar(3)],
|
||||||
|
context.events
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
342
libretrogd/src/graphics/bitmap/blit.rs
Normal file
342
libretrogd/src/graphics/bitmap/blit.rs
Normal file
|
@ -0,0 +1,342 @@
|
||||||
|
use crate::{Bitmap, Rect};
|
||||||
|
|
||||||
|
pub enum BlitMethod {
|
||||||
|
Solid,
|
||||||
|
Transparent(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clips the region for a source bitmap to be used in a subsequent blit operation. The source
|
||||||
|
/// region will be clipped against the clipping region given for the destination bitmap. The
|
||||||
|
/// top-left coordinates of the location to blit to on the destination bitmap are also adjusted
|
||||||
|
/// only if necessary based on the clipping performed.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `dest_clip_region`: the clipping region for the destination bitmap
|
||||||
|
/// * `src_blit_region`: the region on the source bitmap that is to be blitted, which may be
|
||||||
|
/// clipped if necessary to at least partially fit into the destination clipping region given
|
||||||
|
/// * `dest_x`: the x (left) coordinate of the location on the destination bitmap to blit the
|
||||||
|
/// source to, which may be adjusted as necessary during clipping
|
||||||
|
/// * `dest_y`: the y (top) coordinate of the location on the destination bitmap to blit the source
|
||||||
|
/// to, which may be adjusted as necessary during clipping
|
||||||
|
///
|
||||||
|
/// returns: true if the results of the clip is partially or entirely visible on the destination
|
||||||
|
/// bitmap, or false if the blit is entirely outside of the destination bitmap (and so no blit
|
||||||
|
/// needs to occur)
|
||||||
|
pub fn clip_blit(
|
||||||
|
dest_clip_region: &Rect,
|
||||||
|
src_blit_region: &mut Rect,
|
||||||
|
dest_x: &mut i32,
|
||||||
|
dest_y: &mut i32,
|
||||||
|
) -> bool {
|
||||||
|
// off the left edge?
|
||||||
|
if *dest_x < dest_clip_region.x {
|
||||||
|
// completely off the left edge?
|
||||||
|
if (*dest_x + src_blit_region.width as i32 - 1) < dest_clip_region.x {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = dest_clip_region.x - *dest_x;
|
||||||
|
src_blit_region.x += offset;
|
||||||
|
src_blit_region.width = (src_blit_region.width as i32 - offset) as u32;
|
||||||
|
*dest_x = dest_clip_region.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// off the right edge?
|
||||||
|
if *dest_x > dest_clip_region.width as i32 - src_blit_region.width as i32 {
|
||||||
|
// completely off the right edge?
|
||||||
|
if *dest_x > dest_clip_region.right() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = *dest_x + src_blit_region.width as i32 - dest_clip_region.width as i32;
|
||||||
|
src_blit_region.width = (src_blit_region.width as i32 - offset) as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// off the top edge?
|
||||||
|
if *dest_y < dest_clip_region.y {
|
||||||
|
// completely off the top edge?
|
||||||
|
if (*dest_y + src_blit_region.height as i32 - 1) < dest_clip_region.y {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = dest_clip_region.y - *dest_y;
|
||||||
|
src_blit_region.y += offset;
|
||||||
|
src_blit_region.height = (src_blit_region.height as i32 - offset) as u32;
|
||||||
|
*dest_y = dest_clip_region.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
// off the bottom edge?
|
||||||
|
if *dest_y > dest_clip_region.height as i32 - src_blit_region.height as i32 {
|
||||||
|
// completely off the bottom edge?
|
||||||
|
if *dest_y > dest_clip_region.bottom() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = *dest_y + src_blit_region.height as i32 - dest_clip_region.height as i32;
|
||||||
|
src_blit_region.height = (src_blit_region.height as i32 - offset) as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bitmap {
|
||||||
|
pub unsafe fn solid_blit(&mut self, src: &Bitmap, src_region: &Rect, dest_x: i32, dest_y: i32) {
|
||||||
|
let src_row_length = src_region.width as usize;
|
||||||
|
let src_pitch = src.width as usize;
|
||||||
|
let dest_pitch = self.width as usize;
|
||||||
|
let mut src_pixels = src.pixels_at_ptr_unchecked(src_region.x, src_region.y);
|
||||||
|
let mut dest_pixels = self.pixels_at_mut_ptr_unchecked(dest_x, dest_y);
|
||||||
|
|
||||||
|
for _ in 0..src_region.height {
|
||||||
|
dest_pixels.copy_from(src_pixels, src_row_length);
|
||||||
|
src_pixels = src_pixels.add(src_pitch);
|
||||||
|
dest_pixels = dest_pixels.add(dest_pitch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub unsafe fn transparent_blit(
|
||||||
|
&mut self,
|
||||||
|
src: &Bitmap,
|
||||||
|
src_region: &Rect,
|
||||||
|
dest_x: i32,
|
||||||
|
dest_y: i32,
|
||||||
|
transparent_color: u8,
|
||||||
|
) {
|
||||||
|
let src_next_row_inc = (src.width - src_region.width) as usize;
|
||||||
|
let dest_next_row_inc = (self.width - src_region.width) as usize;
|
||||||
|
let mut src_pixels = src.pixels_at_ptr_unchecked(src_region.x, src_region.y);
|
||||||
|
let mut dest_pixels = self.pixels_at_mut_ptr_unchecked(dest_x, dest_y);
|
||||||
|
|
||||||
|
for _ in 0..src_region.height {
|
||||||
|
for _ in 0..src_region.width {
|
||||||
|
let pixel = *src_pixels;
|
||||||
|
if pixel != transparent_color {
|
||||||
|
*dest_pixels = pixel;
|
||||||
|
}
|
||||||
|
|
||||||
|
src_pixels = src_pixels.add(1);
|
||||||
|
dest_pixels = dest_pixels.add(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
src_pixels = src_pixels.add(src_next_row_inc);
|
||||||
|
dest_pixels = dest_pixels.add(dest_next_row_inc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blit_region(
|
||||||
|
&mut self,
|
||||||
|
method: BlitMethod,
|
||||||
|
src: &Bitmap,
|
||||||
|
src_region: &Rect,
|
||||||
|
mut dest_x: i32,
|
||||||
|
mut dest_y: i32,
|
||||||
|
) {
|
||||||
|
let mut src_region = *src_region;
|
||||||
|
if !clip_blit(
|
||||||
|
self.clip_region(),
|
||||||
|
&mut src_region,
|
||||||
|
&mut dest_x,
|
||||||
|
&mut dest_y,
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
self.blit_region_unchecked(method, src, &src_region, dest_x, dest_y);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn blit_region_unchecked(
|
||||||
|
&mut self,
|
||||||
|
method: BlitMethod,
|
||||||
|
src: &Bitmap,
|
||||||
|
src_region: &Rect,
|
||||||
|
dest_x: i32,
|
||||||
|
dest_y: i32,
|
||||||
|
) {
|
||||||
|
use BlitMethod::*;
|
||||||
|
match method {
|
||||||
|
Solid => self.solid_blit(src, src_region, dest_x, dest_y),
|
||||||
|
Transparent(transparent_color) => {
|
||||||
|
self.transparent_blit(src, src_region, dest_x, dest_y, transparent_color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn blit(&mut self, method: BlitMethod, src: &Bitmap, x: i32, y: i32) {
|
||||||
|
let src_region = Rect::new(0, 0, src.width, src.height);
|
||||||
|
self.blit_region(method, src, &src_region, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn blit_unchecked(&mut self, method: BlitMethod, src: &Bitmap, x: i32, y: i32) {
|
||||||
|
let src_region = Rect::new(0, 0, src.width, src.height);
|
||||||
|
self.blit_region_unchecked(method, src, &src_region, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn clip_blit_regions() {
|
||||||
|
let dest = Rect::new(0, 0, 320, 240);
|
||||||
|
|
||||||
|
let mut src: Rect;
|
||||||
|
let mut x: i32;
|
||||||
|
let mut y: i32;
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 10;
|
||||||
|
y = 10;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(0, 0, 16, 16));
|
||||||
|
assert_eq!(10, x);
|
||||||
|
assert_eq!(10, y);
|
||||||
|
|
||||||
|
// left edge
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 0;
|
||||||
|
y = 10;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(0, 0, 16, 16));
|
||||||
|
assert_eq!(0, x);
|
||||||
|
assert_eq!(10, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = -5;
|
||||||
|
y = 10;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(5, 0, 11, 16));
|
||||||
|
assert_eq!(0, x);
|
||||||
|
assert_eq!(10, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = -16;
|
||||||
|
y = 10;
|
||||||
|
assert!(!clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
|
||||||
|
// right edge
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 304;
|
||||||
|
y = 10;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(0, 0, 16, 16));
|
||||||
|
assert_eq!(304, x);
|
||||||
|
assert_eq!(10, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 310;
|
||||||
|
y = 10;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(0, 0, 10, 16));
|
||||||
|
assert_eq!(310, x);
|
||||||
|
assert_eq!(10, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 320;
|
||||||
|
y = 10;
|
||||||
|
assert!(!clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
|
||||||
|
// top edge
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 10;
|
||||||
|
y = 0;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(0, 0, 16, 16));
|
||||||
|
assert_eq!(10, x);
|
||||||
|
assert_eq!(0, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 10;
|
||||||
|
y = -5;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(0, 5, 16, 11));
|
||||||
|
assert_eq!(10, x);
|
||||||
|
assert_eq!(0, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 10;
|
||||||
|
y = -16;
|
||||||
|
assert!(!clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
|
||||||
|
// bottom edge
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 10;
|
||||||
|
y = 224;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(0, 0, 16, 16));
|
||||||
|
assert_eq!(10, x);
|
||||||
|
assert_eq!(224, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 10;
|
||||||
|
y = 229;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(0, 0, 16, 11));
|
||||||
|
assert_eq!(10, x);
|
||||||
|
assert_eq!(229, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 16, 16);
|
||||||
|
x = 10;
|
||||||
|
y = 240;
|
||||||
|
assert!(!clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
|
||||||
|
src = Rect::new(16, 16, 16, 16);
|
||||||
|
x = -1;
|
||||||
|
y = 112;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(17, 16, 15, 16));
|
||||||
|
assert_eq!(0, x);
|
||||||
|
assert_eq!(112, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn clip_blit_regions_large_source() {
|
||||||
|
let dest = Rect::new(0, 0, 64, 64);
|
||||||
|
|
||||||
|
let mut src: Rect;
|
||||||
|
let mut x: i32;
|
||||||
|
let mut y: i32;
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 128, 128);
|
||||||
|
x = 0;
|
||||||
|
y = 0;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(0, 0, 64, 64));
|
||||||
|
assert_eq!(0, x);
|
||||||
|
assert_eq!(0, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 128, 128);
|
||||||
|
x = -16;
|
||||||
|
y = -24;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(16, 24, 64, 64));
|
||||||
|
assert_eq!(0, x);
|
||||||
|
assert_eq!(0, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 32, 128);
|
||||||
|
x = 10;
|
||||||
|
y = -20;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(0, 20, 32, 64));
|
||||||
|
assert_eq!(10, x);
|
||||||
|
assert_eq!(0, y);
|
||||||
|
|
||||||
|
src = Rect::new(0, 0, 128, 32);
|
||||||
|
x = -20;
|
||||||
|
y = 10;
|
||||||
|
assert!(clip_blit(&dest, &mut src, &mut x, &mut y));
|
||||||
|
assert_eq!(src, Rect::new(20, 0, 64, 32));
|
||||||
|
assert_eq!(0, x);
|
||||||
|
assert_eq!(10, y);
|
||||||
|
}
|
||||||
|
}
|
620
libretrogd/src/graphics/bitmap/iff.rs
Normal file
620
libretrogd/src/graphics/bitmap/iff.rs
Normal file
|
@ -0,0 +1,620 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::utils::packbits::{pack_bits, unpack_bits, PackBitsError};
|
||||||
|
use crate::{Bitmap, Palette, PaletteError, PaletteFormat};
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum IffError {
|
||||||
|
#[error("Bad or unsupported IFF file: {0}")]
|
||||||
|
BadFile(String),
|
||||||
|
|
||||||
|
#[error("IFF palette data error")]
|
||||||
|
BadPalette(#[from] PaletteError),
|
||||||
|
|
||||||
|
#[error("PackBits error")]
|
||||||
|
PackBitsError(#[from] PackBitsError),
|
||||||
|
|
||||||
|
#[error("IFF I/O error")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum IffFormat {
|
||||||
|
Pbm,
|
||||||
|
PbmUncompressed,
|
||||||
|
Ilbm,
|
||||||
|
IlbmUncompressed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IffFormat {
|
||||||
|
pub fn compressed(&self) -> bool {
|
||||||
|
use IffFormat::*;
|
||||||
|
match self {
|
||||||
|
Pbm | Ilbm => true,
|
||||||
|
PbmUncompressed | IlbmUncompressed => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chunky(&self) -> bool {
|
||||||
|
use IffFormat::*;
|
||||||
|
match self {
|
||||||
|
Pbm | PbmUncompressed => true,
|
||||||
|
Ilbm | IlbmUncompressed => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn type_id(&self) -> [u8; 4] {
|
||||||
|
use IffFormat::*;
|
||||||
|
match self {
|
||||||
|
Pbm | PbmUncompressed => *b"PBM ",
|
||||||
|
Ilbm | IlbmUncompressed => *b"ILBM",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
#[repr(packed)]
|
||||||
|
struct IffId {
|
||||||
|
id: [u8; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IffId {
|
||||||
|
pub fn read<T: Read>(reader: &mut T) -> Result<Self, IffError> {
|
||||||
|
let mut id = [0u8; 4];
|
||||||
|
reader.read_exact(&mut id)?;
|
||||||
|
Ok(IffId { id })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<T: Write>(&self, writer: &mut T) -> Result<(), IffError> {
|
||||||
|
writer.write_all(&self.id)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
#[repr(packed)]
|
||||||
|
struct FormChunkHeader {
|
||||||
|
chunk_id: IffId,
|
||||||
|
size: u32,
|
||||||
|
type_id: IffId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormChunkHeader {
|
||||||
|
pub fn read<T: ReadBytesExt>(reader: &mut T) -> Result<Self, IffError> {
|
||||||
|
let chunk_id = IffId::read(reader)?;
|
||||||
|
let size = reader.read_u32::<BigEndian>()?;
|
||||||
|
let type_id = IffId::read(reader)?;
|
||||||
|
Ok(FormChunkHeader {
|
||||||
|
chunk_id,
|
||||||
|
size,
|
||||||
|
type_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), IffError> {
|
||||||
|
self.chunk_id.write(writer)?;
|
||||||
|
writer.write_u32::<BigEndian>(self.size)?;
|
||||||
|
self.type_id.write(writer)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
#[repr(packed)]
|
||||||
|
struct SubChunkHeader {
|
||||||
|
chunk_id: IffId,
|
||||||
|
size: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubChunkHeader {
|
||||||
|
pub fn read<T: ReadBytesExt>(reader: &mut T) -> Result<Self, IffError> {
|
||||||
|
let chunk_id = IffId::read(reader)?;
|
||||||
|
let mut size = reader.read_u32::<BigEndian>()?;
|
||||||
|
if (size & 1) == 1 {
|
||||||
|
size += 1; // account for the padding byte
|
||||||
|
}
|
||||||
|
Ok(SubChunkHeader { chunk_id, size })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), IffError> {
|
||||||
|
self.chunk_id.write(writer)?;
|
||||||
|
writer.write_u32::<BigEndian>(self.size)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
#[repr(packed)]
|
||||||
|
struct BMHDChunk {
|
||||||
|
width: u16,
|
||||||
|
height: u16,
|
||||||
|
left: u16,
|
||||||
|
top: u16,
|
||||||
|
bitplanes: u8,
|
||||||
|
masking: u8,
|
||||||
|
compress: u8,
|
||||||
|
padding: u8,
|
||||||
|
transparency: u16,
|
||||||
|
x_aspect_ratio: u8,
|
||||||
|
y_aspect_ratio: u8,
|
||||||
|
page_width: u16,
|
||||||
|
page_height: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BMHDChunk {
|
||||||
|
pub fn read<T: ReadBytesExt>(reader: &mut T) -> Result<Self, IffError> {
|
||||||
|
Ok(BMHDChunk {
|
||||||
|
width: reader.read_u16::<BigEndian>()?,
|
||||||
|
height: reader.read_u16::<BigEndian>()?,
|
||||||
|
left: reader.read_u16::<BigEndian>()?,
|
||||||
|
top: reader.read_u16::<BigEndian>()?,
|
||||||
|
bitplanes: reader.read_u8()?,
|
||||||
|
masking: reader.read_u8()?,
|
||||||
|
compress: reader.read_u8()?,
|
||||||
|
padding: reader.read_u8()?,
|
||||||
|
transparency: reader.read_u16::<BigEndian>()?,
|
||||||
|
x_aspect_ratio: reader.read_u8()?,
|
||||||
|
y_aspect_ratio: reader.read_u8()?,
|
||||||
|
page_width: reader.read_u16::<BigEndian>()?,
|
||||||
|
page_height: reader.read_u16::<BigEndian>()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), IffError> {
|
||||||
|
writer.write_u16::<BigEndian>(self.width)?;
|
||||||
|
writer.write_u16::<BigEndian>(self.height)?;
|
||||||
|
writer.write_u16::<BigEndian>(self.left)?;
|
||||||
|
writer.write_u16::<BigEndian>(self.top)?;
|
||||||
|
writer.write_u8(self.bitplanes)?;
|
||||||
|
writer.write_u8(self.masking)?;
|
||||||
|
writer.write_u8(self.compress)?;
|
||||||
|
writer.write_u8(self.padding)?;
|
||||||
|
writer.write_u16::<BigEndian>(self.transparency)?;
|
||||||
|
writer.write_u8(self.x_aspect_ratio)?;
|
||||||
|
writer.write_u8(self.y_aspect_ratio)?;
|
||||||
|
writer.write_u16::<BigEndian>(self.page_width)?;
|
||||||
|
writer.write_u16::<BigEndian>(self.page_height)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_bitplane(plane: u32, src: &[u8], dest: &mut [u8], row_size: usize) {
|
||||||
|
let bitmask = 1 << plane;
|
||||||
|
for x in 0..row_size {
|
||||||
|
let data = src[x];
|
||||||
|
if (data & 128) > 0 {
|
||||||
|
dest[x * 8] |= bitmask;
|
||||||
|
}
|
||||||
|
if (data & 64) > 0 {
|
||||||
|
dest[(x * 8) + 1] |= bitmask;
|
||||||
|
}
|
||||||
|
if (data & 32) > 0 {
|
||||||
|
dest[(x * 8) + 2] |= bitmask;
|
||||||
|
}
|
||||||
|
if (data & 16) > 0 {
|
||||||
|
dest[(x * 8) + 3] |= bitmask;
|
||||||
|
}
|
||||||
|
if (data & 8) > 0 {
|
||||||
|
dest[(x * 8) + 4] |= bitmask;
|
||||||
|
}
|
||||||
|
if (data & 4) > 0 {
|
||||||
|
dest[(x * 8) + 5] |= bitmask;
|
||||||
|
}
|
||||||
|
if (data & 2) > 0 {
|
||||||
|
dest[(x * 8) + 6] |= bitmask;
|
||||||
|
}
|
||||||
|
if (data & 1) > 0 {
|
||||||
|
dest[(x * 8) + 7] |= bitmask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extract_bitplane(plane: u32, src: &[u8], dest: &mut [u8], row_size: usize) {
|
||||||
|
let bitmask = 1 << plane;
|
||||||
|
let mut src_base_index = 0;
|
||||||
|
for x in 0..row_size {
|
||||||
|
let mut data = 0;
|
||||||
|
if src[src_base_index] & bitmask != 0 {
|
||||||
|
data |= 128;
|
||||||
|
}
|
||||||
|
if src[src_base_index + 1] & bitmask != 0 {
|
||||||
|
data |= 64;
|
||||||
|
}
|
||||||
|
if src[src_base_index + 2] & bitmask != 0 {
|
||||||
|
data |= 32;
|
||||||
|
}
|
||||||
|
if src[src_base_index + 3] & bitmask != 0 {
|
||||||
|
data |= 16;
|
||||||
|
}
|
||||||
|
if src[src_base_index + 4] & bitmask != 0 {
|
||||||
|
data |= 8;
|
||||||
|
}
|
||||||
|
if src[src_base_index + 5] & bitmask != 0 {
|
||||||
|
data |= 4;
|
||||||
|
}
|
||||||
|
if src[src_base_index + 6] & bitmask != 0 {
|
||||||
|
data |= 2;
|
||||||
|
}
|
||||||
|
if src[src_base_index + 7] & bitmask != 0 {
|
||||||
|
data |= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
src_base_index += 8;
|
||||||
|
dest[x] = data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_planar_body<T: ReadBytesExt>(reader: &mut T, bmhd: &BMHDChunk) -> Result<Bitmap, IffError> {
|
||||||
|
let mut bitmap = Bitmap::new(bmhd.width as u32, bmhd.height as u32).unwrap();
|
||||||
|
|
||||||
|
let row_bytes = (((bmhd.width + 15) >> 4) << 1) as usize;
|
||||||
|
let mut buffer = vec![0u8; row_bytes];
|
||||||
|
|
||||||
|
for y in 0..bmhd.height {
|
||||||
|
// planar data is stored for each bitplane in sequence for the scanline.
|
||||||
|
// that is, ALL of bitplane1, followed by ALL of bitplane2, etc, NOT
|
||||||
|
// alternating after each pixel. if compression is enabled, it does NOT
|
||||||
|
// cross bitplane boundaries. each bitplane is compressed individually.
|
||||||
|
// bitplanes also do NOT cross the scanline boundary. basically, each
|
||||||
|
// scanline of pixel data, and within that, each of the bitplanes of
|
||||||
|
// pixel data found in each scanline can all be treated as they are all
|
||||||
|
// their own self-contained bit of data as far as this loading process
|
||||||
|
// is concerned (well, except that we merge all of the scanline's
|
||||||
|
// bitplanes together at the end of each line)
|
||||||
|
|
||||||
|
// read all the bitplane rows per scanline
|
||||||
|
for plane in 0..(bmhd.bitplanes as u32) {
|
||||||
|
if bmhd.compress == 1 {
|
||||||
|
// decompress packed line for this bitplane only
|
||||||
|
buffer.clear();
|
||||||
|
unpack_bits(reader, &mut buffer, row_bytes)?
|
||||||
|
} else {
|
||||||
|
// TODO: check this. maybe row_bytes calculation is wrong? either way, i don't
|
||||||
|
// think that DP2 or Grafx2 ever output uncompressed interleaved files ...
|
||||||
|
// just read all this bitplane's line data in as-is
|
||||||
|
reader.read_exact(&mut buffer)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// merge this bitplane data into the final destination. after all of
|
||||||
|
// the bitplanes have been loaded and merged in this way for this
|
||||||
|
// scanline, the destination pointer will contain VGA-friendly
|
||||||
|
// "chunky pixel"-format pixel data
|
||||||
|
merge_bitplane(
|
||||||
|
plane,
|
||||||
|
&buffer,
|
||||||
|
bitmap.pixels_at_mut(0, y as i32).unwrap(),
|
||||||
|
row_bytes,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_chunky_body<T: ReadBytesExt>(reader: &mut T, bmhd: &BMHDChunk) -> Result<Bitmap, IffError> {
|
||||||
|
let mut bitmap = Bitmap::new(bmhd.width as u32, bmhd.height as u32).unwrap();
|
||||||
|
|
||||||
|
for y in 0..bmhd.height {
|
||||||
|
if bmhd.compress == 1 {
|
||||||
|
// for compression-enabled, read row of pixels using PackBits
|
||||||
|
let mut writer = bitmap.pixels_at_mut(0, y as i32).unwrap();
|
||||||
|
unpack_bits(reader, &mut writer, bmhd.width as usize)?
|
||||||
|
} else {
|
||||||
|
// for uncompressed, read row of pixels literally
|
||||||
|
let dest = &mut bitmap.pixels_at_mut(0, y as i32).unwrap()[0..bmhd.width as usize];
|
||||||
|
reader.read_exact(dest)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(bitmap)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_planar_body<T: WriteBytesExt>(
|
||||||
|
writer: &mut T,
|
||||||
|
bitmap: &Bitmap,
|
||||||
|
bmhd: &BMHDChunk,
|
||||||
|
) -> Result<(), IffError> {
|
||||||
|
let row_bytes = (((bitmap.width() + 15) >> 4) << 1) as usize;
|
||||||
|
let mut buffer = vec![0u8; row_bytes];
|
||||||
|
|
||||||
|
for y in 0..bitmap.height() {
|
||||||
|
for plane in 0..(bmhd.bitplanes as u32) {
|
||||||
|
extract_bitplane(
|
||||||
|
plane,
|
||||||
|
bitmap.pixels_at(0, y as i32).unwrap(),
|
||||||
|
&mut buffer,
|
||||||
|
row_bytes,
|
||||||
|
);
|
||||||
|
|
||||||
|
if bmhd.compress == 1 {
|
||||||
|
// for compression-enabled, write this plane's pixels using PackBits
|
||||||
|
pack_bits(&mut buffer.as_slice(), writer, row_bytes)?;
|
||||||
|
} else {
|
||||||
|
// TODO: check this. maybe row_bytes calculation is wrong? either way, i don't
|
||||||
|
// think that DP2 or Grafx2 ever output uncompressed interleaved files ...
|
||||||
|
// for uncompressed, write this plane's pixels literally
|
||||||
|
writer.write_all(&buffer)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_chunky_body<T: WriteBytesExt>(
|
||||||
|
writer: &mut T,
|
||||||
|
bitmap: &Bitmap,
|
||||||
|
bmhd: &BMHDChunk,
|
||||||
|
) -> Result<(), IffError> {
|
||||||
|
for y in 0..bitmap.height() {
|
||||||
|
if bmhd.compress == 1 {
|
||||||
|
// for compression-enabled, write row of pixels using PackBits
|
||||||
|
let mut reader = bitmap.pixels_at(0, y as i32).unwrap();
|
||||||
|
pack_bits(&mut reader, writer, bitmap.width() as usize)?;
|
||||||
|
} else {
|
||||||
|
// for uncompressed, write out the row of pixels literally
|
||||||
|
let src = &bitmap.pixels_at(0, y as i32).unwrap()[0..bitmap.width() as usize];
|
||||||
|
writer.write_all(src)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bitmap {
|
||||||
|
pub fn load_iff_bytes<T: ReadBytesExt + Seek>(
|
||||||
|
reader: &mut T,
|
||||||
|
) -> Result<(Bitmap, Palette), IffError> {
|
||||||
|
let form_chunk = FormChunkHeader::read(reader)?;
|
||||||
|
if form_chunk.chunk_id.id != *b"FORM" {
|
||||||
|
return Err(IffError::BadFile(String::from(
|
||||||
|
"Unexpected form chunk ID, probably not an IFF file",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if form_chunk.type_id.id != *b"ILBM" && form_chunk.type_id.id != *b"PBM " {
|
||||||
|
return Err(IffError::BadFile(String::from(
|
||||||
|
"Only ILBM or PBM formats are supported",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bmhd: Option<BMHDChunk> = None;
|
||||||
|
let mut palette: Option<Palette> = None;
|
||||||
|
let mut bitmap: Option<Bitmap> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let header = match SubChunkHeader::read(reader) {
|
||||||
|
Ok(header) => header,
|
||||||
|
Err(IffError::IOError(io_error))
|
||||||
|
if io_error.kind() == io::ErrorKind::UnexpectedEof =>
|
||||||
|
{
|
||||||
|
break
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err),
|
||||||
|
};
|
||||||
|
let chunk_data_position = reader.stream_position()?;
|
||||||
|
|
||||||
|
// todo: process chunk here
|
||||||
|
if header.chunk_id.id == *b"BMHD" {
|
||||||
|
bmhd = Some(BMHDChunk::read(reader)?);
|
||||||
|
if bmhd.as_ref().unwrap().bitplanes != 8 {
|
||||||
|
return Err(IffError::BadFile(String::from(
|
||||||
|
"Only 8bpp files are supported",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if bmhd.as_ref().unwrap().masking == 1 {
|
||||||
|
return Err(IffError::BadFile(String::from("Masking is not supported")));
|
||||||
|
}
|
||||||
|
} else if header.chunk_id.id == *b"CMAP" {
|
||||||
|
if header.size != 768 {
|
||||||
|
return Err(IffError::BadFile(String::from(
|
||||||
|
"Only 256 color files are supported",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
palette = Some(Palette::load_from_bytes(reader, PaletteFormat::Normal)?)
|
||||||
|
} else if header.chunk_id.id == *b"BODY" {
|
||||||
|
if let Some(bmhd) = &bmhd {
|
||||||
|
if form_chunk.type_id.id == *b"PBM " {
|
||||||
|
bitmap = Some(load_chunky_body(reader, bmhd)?);
|
||||||
|
} else {
|
||||||
|
bitmap = Some(load_planar_body(reader, bmhd)?);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: does this ever occur in practice? and if so, we can probably make some
|
||||||
|
// changes to allow for it ...
|
||||||
|
return Err(IffError::BadFile(String::from(
|
||||||
|
"BODY chunk occurs before BMHD chunk, or no BMHD chunk exists",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.seek(SeekFrom::Start(chunk_data_position + header.size as u64))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if bitmap.is_none() {
|
||||||
|
return Err(IffError::BadFile(String::from("No BODY chunk was found")));
|
||||||
|
}
|
||||||
|
// TODO: we can probably make this optional ...
|
||||||
|
if palette.is_none() {
|
||||||
|
return Err(IffError::BadFile(String::from("No CMAP chunk was found")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((bitmap.unwrap(), palette.unwrap()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_iff_file(path: &Path) -> Result<(Bitmap, Palette), IffError> {
|
||||||
|
let f = File::open(path)?;
|
||||||
|
let mut reader = BufReader::new(f);
|
||||||
|
Self::load_iff_bytes(&mut reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_iff_bytes<T: WriteBytesExt + Seek>(
|
||||||
|
&self,
|
||||||
|
writer: &mut T,
|
||||||
|
palette: &Palette,
|
||||||
|
format: IffFormat,
|
||||||
|
) -> Result<(), IffError> {
|
||||||
|
let form_chunk_position = writer.stream_position()?;
|
||||||
|
|
||||||
|
let mut form_chunk = FormChunkHeader {
|
||||||
|
chunk_id: IffId { id: *b"FORM" },
|
||||||
|
type_id: IffId {
|
||||||
|
id: format.type_id(),
|
||||||
|
},
|
||||||
|
size: 0, // filled in later once we know the size
|
||||||
|
};
|
||||||
|
|
||||||
|
// skip over the form chunk for now. will come back here and write it out later once we
|
||||||
|
// know what the final size is
|
||||||
|
writer.seek(SeekFrom::Current(
|
||||||
|
std::mem::size_of::<FormChunkHeader>() as i64
|
||||||
|
))?;
|
||||||
|
|
||||||
|
let bmhd_chunk_header = SubChunkHeader {
|
||||||
|
chunk_id: IffId { id: *b"BMHD" },
|
||||||
|
size: std::mem::size_of::<BMHDChunk>() as u32,
|
||||||
|
};
|
||||||
|
let bmhd = BMHDChunk {
|
||||||
|
width: self.width() as u16,
|
||||||
|
height: self.height() as u16,
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
bitplanes: 8,
|
||||||
|
masking: 0,
|
||||||
|
compress: if format.compressed() { 1 } else { 0 },
|
||||||
|
padding: 0,
|
||||||
|
transparency: 0,
|
||||||
|
// the following values are based on what DP2 writes out in 320x200 modes. good enough.
|
||||||
|
x_aspect_ratio: 5,
|
||||||
|
y_aspect_ratio: 6,
|
||||||
|
page_width: 320,
|
||||||
|
page_height: 200,
|
||||||
|
};
|
||||||
|
bmhd_chunk_header.write(writer)?;
|
||||||
|
bmhd.write(writer)?;
|
||||||
|
|
||||||
|
let cmap_chunk_header = SubChunkHeader {
|
||||||
|
chunk_id: IffId { id: *b"CMAP" },
|
||||||
|
size: 768,
|
||||||
|
};
|
||||||
|
cmap_chunk_header.write(writer)?;
|
||||||
|
palette.to_bytes(writer, PaletteFormat::Normal)?;
|
||||||
|
|
||||||
|
let body_position = writer.stream_position()?;
|
||||||
|
|
||||||
|
let mut body_chunk_header = SubChunkHeader {
|
||||||
|
chunk_id: IffId { id: *b"BODY" },
|
||||||
|
size: 0, // filled in later once we know the size
|
||||||
|
};
|
||||||
|
|
||||||
|
// skip over the body chunk header for now. we will again come back here and write it out
|
||||||
|
// later once we know what the final size again.
|
||||||
|
writer.seek(SeekFrom::Current(
|
||||||
|
std::mem::size_of::<SubChunkHeader>() as i64
|
||||||
|
))?;
|
||||||
|
|
||||||
|
if format.chunky() {
|
||||||
|
write_chunky_body(writer, self, &bmhd)?;
|
||||||
|
} else {
|
||||||
|
write_planar_body(writer, self, &bmhd)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a padding byte (only if necessary) to the body we just finished writing
|
||||||
|
let mut eof_pos = writer.stream_position()?;
|
||||||
|
if (eof_pos - body_position) & 1 == 1 {
|
||||||
|
writer.write_u8(0)?;
|
||||||
|
eof_pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// go back and write out the form chunk header now that we know the final file size
|
||||||
|
form_chunk.size = (eof_pos - (std::mem::size_of::<IffId>() as u64 * 2)) as u32;
|
||||||
|
writer.seek(SeekFrom::Start(form_chunk_position))?;
|
||||||
|
form_chunk.write(writer)?;
|
||||||
|
|
||||||
|
// and then write out the body chunk header since we now know the size of that too
|
||||||
|
body_chunk_header.size = eof_pos as u32 - std::mem::size_of::<SubChunkHeader>() as u32;
|
||||||
|
writer.seek(SeekFrom::Start(body_position))?;
|
||||||
|
body_chunk_header.write(writer)?;
|
||||||
|
|
||||||
|
// and then go back to eof
|
||||||
|
writer.seek(SeekFrom::Start(eof_pos))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_iff_file(
|
||||||
|
&self,
|
||||||
|
path: &Path,
|
||||||
|
palette: &Palette,
|
||||||
|
format: IffFormat,
|
||||||
|
) -> Result<(), IffError> {
|
||||||
|
let f = File::create(path)?;
|
||||||
|
let mut writer = BufWriter::new(f);
|
||||||
|
self.to_iff_bytes(&mut writer, palette, format)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub static TEST_BMP_PIXELS_RAW: &[u8] =
|
||||||
|
include_bytes!("../../../test-assets/test_bmp_pixels_raw.bin");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn load_and_save() -> Result<(), IffError> {
|
||||||
|
let dp2_palette =
|
||||||
|
Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal)
|
||||||
|
.unwrap();
|
||||||
|
let tmp_dir = TempDir::new()?;
|
||||||
|
|
||||||
|
// ILBM format
|
||||||
|
|
||||||
|
let (bmp, palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_ilbm.lbm"))?;
|
||||||
|
assert_eq!(16, bmp.width());
|
||||||
|
assert_eq!(16, bmp.height());
|
||||||
|
assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW);
|
||||||
|
assert_eq!(palette, dp2_palette);
|
||||||
|
|
||||||
|
let save_path = tmp_dir.path().join("test_save_ilbm.lbm");
|
||||||
|
bmp.to_iff_file(&save_path, &palette, IffFormat::Ilbm)?;
|
||||||
|
let (reloaded_bmp, reloaded_palette) = Bitmap::load_iff_file(&save_path)?;
|
||||||
|
assert_eq!(16, reloaded_bmp.width());
|
||||||
|
assert_eq!(16, reloaded_bmp.height());
|
||||||
|
assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW);
|
||||||
|
assert_eq!(reloaded_palette, dp2_palette);
|
||||||
|
|
||||||
|
// PBM format
|
||||||
|
|
||||||
|
let (bmp, palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_pbm.lbm"))?;
|
||||||
|
assert_eq!(16, bmp.width());
|
||||||
|
assert_eq!(16, bmp.height());
|
||||||
|
assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW);
|
||||||
|
assert_eq!(palette, dp2_palette);
|
||||||
|
|
||||||
|
let save_path = tmp_dir.path().join("test_save_pbm.lbm");
|
||||||
|
bmp.to_iff_file(&save_path, &palette, IffFormat::Pbm)?;
|
||||||
|
let (reloaded_bmp, reloaded_palette) = Bitmap::load_iff_file(&save_path)?;
|
||||||
|
assert_eq!(16, reloaded_bmp.width());
|
||||||
|
assert_eq!(16, reloaded_bmp.height());
|
||||||
|
assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW);
|
||||||
|
assert_eq!(reloaded_palette, dp2_palette);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn load_larger_image() -> Result<(), IffError> {
|
||||||
|
let (bmp, _palette) = Bitmap::load_iff_file(Path::new("./test-assets/test_image.lbm"))?;
|
||||||
|
assert_eq!(320, bmp.width());
|
||||||
|
assert_eq!(200, bmp.height());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
584
libretrogd/src/graphics/bitmap/mod.rs
Normal file
584
libretrogd/src/graphics/bitmap/mod.rs
Normal file
|
@ -0,0 +1,584 @@
|
||||||
|
use std::path::Path;
|
||||||
|
use std::slice;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub use crate::blit::*;
|
||||||
|
pub use crate::iff::*;
|
||||||
|
pub use crate::pcx::*;
|
||||||
|
pub use crate::primitives::*;
|
||||||
|
use crate::{Palette, Rect};
|
||||||
|
|
||||||
|
pub mod blit;
|
||||||
|
pub mod iff;
|
||||||
|
pub mod pcx;
|
||||||
|
pub mod primitives;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum BitmapError {
|
||||||
|
#[error("Invalid bitmap dimensions")]
|
||||||
|
InvalidDimensions,
|
||||||
|
|
||||||
|
#[error("Region is not fully within bitmap boundaries")]
|
||||||
|
OutOfBounds,
|
||||||
|
|
||||||
|
#[error("Unknown bitmap file type: {0}")]
|
||||||
|
UnknownFileType(String),
|
||||||
|
|
||||||
|
#[error("Bitmap IFF file error")]
|
||||||
|
IffError(#[from] iff::IffError),
|
||||||
|
|
||||||
|
#[error("Bitmap PCX file error")]
|
||||||
|
PcxError(#[from] pcx::PcxError),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Container for 256 color 2D pixel/image data that can be rendered to the screen. Pixel data
|
||||||
|
/// is stored as contiguous bytes, where each pixel is an index into a separate 256 color palette
|
||||||
|
/// stored independently of the bitmap. The pixel data is not padded in any way, so the stride from
|
||||||
|
/// one row to the next is always exactly equal to the bitmap width. Rendering operations provided
|
||||||
|
/// here are done with respect to the bitmaps clipping region, where rendering outside of the
|
||||||
|
/// clipping region is simply not performed / stops at the clipping boundary.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Bitmap {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
pixels: Box<[u8]>,
|
||||||
|
clip_region: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bitmap {
|
||||||
|
/// Creates a new Bitmap with the specified dimensions.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `width`: the width of the bitmap in pixels
|
||||||
|
/// * `height`: the height of the bitmap in pixels
|
||||||
|
///
|
||||||
|
/// returns: `Result<Bitmap, BitmapError>`
|
||||||
|
pub fn new(width: u32, height: u32) -> Result<Bitmap, BitmapError> {
|
||||||
|
if width == 0 || height == 0 {
|
||||||
|
return Err(BitmapError::InvalidDimensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Bitmap {
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
pixels: vec![0u8; (width * height) as usize].into_boxed_slice(),
|
||||||
|
clip_region: Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Bitmap, copying the pixel data from a sub-region of another source Bitmap.
|
||||||
|
/// The resulting bitmap will have dimensions equal to that of the region specified.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `source`: the source bitmap to copy from
|
||||||
|
/// * `region`: the region on the source bitmap to copy from
|
||||||
|
///
|
||||||
|
/// returns: `Result<Bitmap, BitmapError>`
|
||||||
|
pub fn from(source: &Bitmap, region: &Rect) -> Result<Bitmap, BitmapError> {
|
||||||
|
if !source.full_bounds().contains_rect(region) {
|
||||||
|
return Err(BitmapError::OutOfBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut bmp = Bitmap::new(region.width, region.height)?;
|
||||||
|
unsafe { bmp.solid_blit(source, region, 0, 0) };
|
||||||
|
Ok(bmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_file(path: &Path) -> Result<(Bitmap, Palette), BitmapError> {
|
||||||
|
if let Some(extension) = path.extension() {
|
||||||
|
let extension = extension.to_ascii_lowercase();
|
||||||
|
match extension.to_str() {
|
||||||
|
Some("pcx") => Ok(Self::load_pcx_file(path)?),
|
||||||
|
Some("iff") | Some("lbm") | Some("pbm") | Some("bbm") => {
|
||||||
|
Ok(Self::load_iff_file(path)?)
|
||||||
|
}
|
||||||
|
_ => Err(BitmapError::UnknownFileType(String::from(
|
||||||
|
"Unrecognized file extension",
|
||||||
|
))),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Err(BitmapError::UnknownFileType(String::from(
|
||||||
|
"No file extension",
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the width of the bitmap in pixels.
|
||||||
|
#[inline]
|
||||||
|
pub fn width(&self) -> u32 {
|
||||||
|
self.width
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the height of the bitmap in pixels.
|
||||||
|
#[inline]
|
||||||
|
pub fn height(&self) -> u32 {
|
||||||
|
self.height
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the right x coordinate of the bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub fn right(&self) -> u32 {
|
||||||
|
self.width - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the bottom x coordinate of the bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub fn bottom(&self) -> u32 {
|
||||||
|
self.height - 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current clipping region set on this bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub fn clip_region(&self) -> &Rect {
|
||||||
|
&self.clip_region
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a rect representing the full bitmap boundaries, ignoring the current clipping
|
||||||
|
/// region set on this bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub fn full_bounds(&self) -> Rect {
|
||||||
|
Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: self.width,
|
||||||
|
height: self.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a new clipping region on this bitmap. The region will be automatically clamped to
|
||||||
|
/// the maximum bitmap boundaries if the supplied region extends beyond it.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `region`: the new clipping region
|
||||||
|
pub fn set_clip_region(&mut self, region: &Rect) {
|
||||||
|
self.clip_region = *region;
|
||||||
|
self.clip_region.clamp_to(&self.full_bounds());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the bitmaps clipping region back to the default (full boundaries of the bitmap).
|
||||||
|
pub fn reset_clip_region(&mut self) {
|
||||||
|
self.clip_region = self.full_bounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the raw pixels in this bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub fn pixels(&self) -> &[u8] {
|
||||||
|
&self.pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the raw pixels in this bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub fn pixels_mut(&mut self) -> &mut [u8] {
|
||||||
|
&mut self.pixels
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the subset of the raw pixels in this bitmap beginning at the
|
||||||
|
/// given coordinates and extending to the end of the bitmap. If the coordinates given are
|
||||||
|
/// outside the bitmap's current clipping region, None is returned.
|
||||||
|
#[inline]
|
||||||
|
pub fn pixels_at(&self, x: i32, y: i32) -> Option<&[u8]> {
|
||||||
|
if self.is_xy_visible(x, y) {
|
||||||
|
let offset = self.get_offset_to_xy(x, y);
|
||||||
|
Some(&self.pixels[offset..])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable reference to the subset of the raw pixels in this bitmap beginning at the
|
||||||
|
/// given coordinates and extending to the end of the bitmap. If the coordinates given are
|
||||||
|
/// outside the bitmap's current clipping region, None is returned.
|
||||||
|
#[inline]
|
||||||
|
pub fn pixels_at_mut(&mut self, x: i32, y: i32) -> Option<&mut [u8]> {
|
||||||
|
if self.is_xy_visible(x, y) {
|
||||||
|
let offset = self.get_offset_to_xy(x, y);
|
||||||
|
Some(&mut self.pixels[offset..])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an unsafe reference to the subset of the raw pixels in this bitmap beginning at the
|
||||||
|
/// given coordinates and extending to the end of the bitmap. The coordinates are not checked
|
||||||
|
/// for validity, so it is up to you to ensure they lie within the bounds of the bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn pixels_at_unchecked(&self, x: i32, y: i32) -> &[u8] {
|
||||||
|
let offset = self.get_offset_to_xy(x, y);
|
||||||
|
slice::from_raw_parts(self.pixels.as_ptr().add(offset), self.pixels.len() - offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable unsafe reference to the subset of the raw pixels in this bitmap beginning
|
||||||
|
/// at the given coordinates and extending to the end of the bitmap. The coordinates are not
|
||||||
|
/// checked for validity, so it is up to you to ensure they lie within the bounds of the bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn pixels_at_mut_unchecked(&mut self, x: i32, y: i32) -> &mut [u8] {
|
||||||
|
let offset = self.get_offset_to_xy(x, y);
|
||||||
|
slice::from_raw_parts_mut(
|
||||||
|
self.pixels.as_mut_ptr().add(offset),
|
||||||
|
self.pixels.len() - offset,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a pointer to the subset of the raw pixels in this bitmap beginning at the given
|
||||||
|
/// coordinates. If the coordinates given are outside the bitmap's current clipping region,
|
||||||
|
/// None is returned.
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn pixels_at_ptr(&self, x: i32, y: i32) -> Option<*const u8> {
|
||||||
|
if self.is_xy_visible(x, y) {
|
||||||
|
let offset = self.get_offset_to_xy(x, y);
|
||||||
|
Some(self.pixels.as_ptr().add(offset))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable pointer to the subset of the raw pixels in this bitmap beginning at the
|
||||||
|
/// given coordinates. If the coordinates given are outside the bitmap's current clipping
|
||||||
|
/// region, None is returned.
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn pixels_at_mut_ptr(&mut self, x: i32, y: i32) -> Option<*mut u8> {
|
||||||
|
if self.is_xy_visible(x, y) {
|
||||||
|
let offset = self.get_offset_to_xy(x, y);
|
||||||
|
Some(self.pixels.as_mut_ptr().add(offset))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an unsafe pointer to the subset of the raw pixels in this bitmap beginning at the
|
||||||
|
/// given coordinates. The coordinates are not checked for validity, so it is up to you to
|
||||||
|
/// ensure they lie within the bounds of the bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn pixels_at_ptr_unchecked(&self, x: i32, y: i32) -> *const u8 {
|
||||||
|
let offset = self.get_offset_to_xy(x, y);
|
||||||
|
self.pixels.as_ptr().add(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a mutable unsafe pointer to the subset of the raw pixels in this bitmap beginning
|
||||||
|
/// at the given coordinates. The coordinates are not checked for validity, so it is up to you
|
||||||
|
/// to ensure they lie within the bounds of the bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn pixels_at_mut_ptr_unchecked(&mut self, x: i32, y: i32) -> *mut u8 {
|
||||||
|
let offset = self.get_offset_to_xy(x, y);
|
||||||
|
self.pixels.as_mut_ptr().add(offset)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an offset corresponding to the coordinates of the pixel given on this bitmap that
|
||||||
|
/// can be used with a reference to the raw pixels in this bitmap to access that pixel. The
|
||||||
|
/// coordinates given are not checked for validity.
|
||||||
|
#[inline]
|
||||||
|
pub fn get_offset_to_xy(&self, x: i32, y: i32) -> usize {
|
||||||
|
((y * self.width as i32) + x) as usize
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the coordinates given lie within the bitmaps clipping region.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_xy_visible(&self, x: i32, y: i32) -> bool {
|
||||||
|
(x >= self.clip_region.x)
|
||||||
|
&& (y >= self.clip_region.y)
|
||||||
|
&& (x <= self.clip_region.right())
|
||||||
|
&& (y <= self.clip_region.bottom())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Copies and converts the entire pixel data from this bitmap to a destination expecting
|
||||||
|
/// 32-bit ARGB-format pixel data. This can be used to display the contents of the bitmap
|
||||||
|
/// on-screen by using an SDL Surface, OpenGL texture, etc as the destination.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `dest`: destination 32-bit ARGB pixel buffer to copy converted pixels to
|
||||||
|
/// * `palette`: the 256 colour palette to use during pixel conversion
|
||||||
|
pub fn copy_as_argb_to(&self, dest: &mut [u32], palette: &Palette) {
|
||||||
|
for (src, dest) in self.pixels().iter().zip(dest.iter_mut()) {
|
||||||
|
*dest = palette[*src];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use claim::assert_matches;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
static RAW_BMP_PIXELS: &[u8] = &[
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 1, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 2,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
static RAW_BMP_PIXELS_SUBSET: &[u8] = &[
|
||||||
|
0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0,
|
||||||
|
0, 0, 0, 2,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn creation_and_sizing() {
|
||||||
|
assert_matches!(Bitmap::new(0, 0), Err(BitmapError::InvalidDimensions));
|
||||||
|
assert_matches!(Bitmap::new(16, 0), Err(BitmapError::InvalidDimensions));
|
||||||
|
assert_matches!(Bitmap::new(0, 32), Err(BitmapError::InvalidDimensions));
|
||||||
|
let bmp = Bitmap::new(16, 32).unwrap();
|
||||||
|
assert_eq!(16, bmp.width());
|
||||||
|
assert_eq!(32, bmp.height());
|
||||||
|
assert_eq!(15, bmp.right());
|
||||||
|
assert_eq!(31, bmp.bottom());
|
||||||
|
assert_eq!(
|
||||||
|
Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 16,
|
||||||
|
height: 32
|
||||||
|
},
|
||||||
|
bmp.full_bounds()
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 16,
|
||||||
|
height: 32
|
||||||
|
},
|
||||||
|
*bmp.clip_region()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn copy_from() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
Bitmap::from(&bmp, &Rect::new(0, 0, 16, 16)),
|
||||||
|
Err(BitmapError::OutOfBounds)
|
||||||
|
);
|
||||||
|
|
||||||
|
let copy = Bitmap::from(&bmp, &Rect::new(0, 0, 8, 8)).unwrap();
|
||||||
|
assert_eq!(bmp.pixels(), copy.pixels());
|
||||||
|
|
||||||
|
let copy = Bitmap::from(&bmp, &Rect::new(4, 4, 4, 4)).unwrap();
|
||||||
|
assert_eq!(RAW_BMP_PIXELS_SUBSET, copy.pixels());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn xy_offset_calculation() {
|
||||||
|
let bmp = Bitmap::new(20, 15).unwrap();
|
||||||
|
assert_eq!(0, bmp.get_offset_to_xy(0, 0));
|
||||||
|
assert_eq!(19, bmp.get_offset_to_xy(19, 0));
|
||||||
|
assert_eq!(20, bmp.get_offset_to_xy(0, 1));
|
||||||
|
assert_eq!(280, bmp.get_offset_to_xy(0, 14));
|
||||||
|
assert_eq!(299, bmp.get_offset_to_xy(19, 14));
|
||||||
|
assert_eq!(227, bmp.get_offset_to_xy(7, 11));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn bounds_testing_and_clip_region() {
|
||||||
|
let mut bmp = Bitmap::new(16, 8).unwrap();
|
||||||
|
assert!(bmp.is_xy_visible(0, 0));
|
||||||
|
assert!(bmp.is_xy_visible(15, 0));
|
||||||
|
assert!(bmp.is_xy_visible(0, 7));
|
||||||
|
assert!(bmp.is_xy_visible(15, 7));
|
||||||
|
assert!(!bmp.is_xy_visible(-1, -1));
|
||||||
|
assert!(!bmp.is_xy_visible(16, 8));
|
||||||
|
assert!(!bmp.is_xy_visible(4, -2));
|
||||||
|
assert!(!bmp.is_xy_visible(11, 8));
|
||||||
|
assert!(!bmp.is_xy_visible(-1, 3));
|
||||||
|
assert!(!bmp.is_xy_visible(16, 6));
|
||||||
|
|
||||||
|
let new_clip_region = Rect::from_coords(4, 2, 12, 6);
|
||||||
|
bmp.set_clip_region(&new_clip_region);
|
||||||
|
assert_eq!(
|
||||||
|
Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 16,
|
||||||
|
height: 8
|
||||||
|
},
|
||||||
|
bmp.full_bounds()
|
||||||
|
);
|
||||||
|
assert_eq!(new_clip_region, *bmp.clip_region());
|
||||||
|
assert!(bmp.is_xy_visible(4, 2));
|
||||||
|
assert!(bmp.is_xy_visible(12, 2));
|
||||||
|
assert!(bmp.is_xy_visible(4, 6));
|
||||||
|
assert!(bmp.is_xy_visible(12, 6));
|
||||||
|
assert!(!bmp.is_xy_visible(3, 1));
|
||||||
|
assert!(!bmp.is_xy_visible(13, 7));
|
||||||
|
assert!(!bmp.is_xy_visible(5, 1));
|
||||||
|
assert!(!bmp.is_xy_visible(10, 7));
|
||||||
|
assert!(!bmp.is_xy_visible(3, 4));
|
||||||
|
assert!(!bmp.is_xy_visible(13, 5));
|
||||||
|
|
||||||
|
assert!(!bmp.is_xy_visible(0, 0));
|
||||||
|
assert!(!bmp.is_xy_visible(15, 0));
|
||||||
|
assert!(!bmp.is_xy_visible(0, 7));
|
||||||
|
assert!(!bmp.is_xy_visible(15, 7));
|
||||||
|
bmp.reset_clip_region();
|
||||||
|
assert!(bmp.is_xy_visible(0, 0));
|
||||||
|
assert!(bmp.is_xy_visible(15, 0));
|
||||||
|
assert!(bmp.is_xy_visible(0, 7));
|
||||||
|
assert!(bmp.is_xy_visible(15, 7));
|
||||||
|
assert!(!bmp.is_xy_visible(-1, -1));
|
||||||
|
assert!(!bmp.is_xy_visible(16, 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn pixels_at() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
|
||||||
|
|
||||||
|
assert_eq!(None, bmp.pixels_at(-1, -1));
|
||||||
|
|
||||||
|
let offset = bmp.get_offset_to_xy(1, 1);
|
||||||
|
let pixels = bmp.pixels_at(0, 0).unwrap();
|
||||||
|
assert_eq!(64, pixels.len());
|
||||||
|
assert_eq!(0, pixels[0]);
|
||||||
|
assert_eq!(1, pixels[offset]);
|
||||||
|
assert_eq!(2, pixels[63]);
|
||||||
|
|
||||||
|
let pixels = bmp.pixels_at(1, 1).unwrap();
|
||||||
|
assert_eq!(55, pixels.len());
|
||||||
|
assert_eq!(1, pixels[0]);
|
||||||
|
assert_eq!(2, pixels[54]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn pixels_at_mut() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
|
||||||
|
|
||||||
|
assert_eq!(None, bmp.pixels_at_mut(-1, -1));
|
||||||
|
|
||||||
|
let offset = bmp.get_offset_to_xy(1, 1);
|
||||||
|
let pixels = bmp.pixels_at_mut(0, 0).unwrap();
|
||||||
|
assert_eq!(64, pixels.len());
|
||||||
|
assert_eq!(0, pixels[0]);
|
||||||
|
assert_eq!(1, pixels[offset]);
|
||||||
|
assert_eq!(2, pixels[63]);
|
||||||
|
|
||||||
|
let pixels = bmp.pixels_at_mut(1, 1).unwrap();
|
||||||
|
assert_eq!(55, pixels.len());
|
||||||
|
assert_eq!(1, pixels[0]);
|
||||||
|
assert_eq!(2, pixels[54]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn pixels_at_unchecked() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
|
||||||
|
|
||||||
|
let offset = bmp.get_offset_to_xy(1, 1);
|
||||||
|
let pixels = unsafe { bmp.pixels_at_unchecked(0, 0) };
|
||||||
|
assert_eq!(64, pixels.len());
|
||||||
|
assert_eq!(0, pixels[0]);
|
||||||
|
assert_eq!(1, pixels[offset]);
|
||||||
|
assert_eq!(2, pixels[63]);
|
||||||
|
|
||||||
|
let pixels = unsafe { bmp.pixels_at_unchecked(1, 1) };
|
||||||
|
assert_eq!(55, pixels.len());
|
||||||
|
assert_eq!(1, pixels[0]);
|
||||||
|
assert_eq!(2, pixels[54]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn pixels_at_mut_unchecked() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
|
||||||
|
|
||||||
|
let offset = bmp.get_offset_to_xy(1, 1);
|
||||||
|
let pixels = unsafe { bmp.pixels_at_mut_unchecked(0, 0) };
|
||||||
|
assert_eq!(64, pixels.len());
|
||||||
|
assert_eq!(0, pixels[0]);
|
||||||
|
assert_eq!(1, pixels[offset]);
|
||||||
|
assert_eq!(2, pixels[63]);
|
||||||
|
|
||||||
|
let pixels = unsafe { bmp.pixels_at_mut_unchecked(1, 1) };
|
||||||
|
assert_eq!(55, pixels.len());
|
||||||
|
assert_eq!(1, pixels[0]);
|
||||||
|
assert_eq!(2, pixels[54]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn pixels_at_ptr() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
|
||||||
|
|
||||||
|
assert_eq!(None, unsafe { bmp.pixels_at_ptr(-1, -1) });
|
||||||
|
|
||||||
|
let offset = bmp.get_offset_to_xy(1, 1);
|
||||||
|
let pixels = unsafe { bmp.pixels_at_ptr(0, 0).unwrap() };
|
||||||
|
assert_eq!(0, unsafe { *pixels });
|
||||||
|
assert_eq!(1, unsafe { *(pixels.add(offset)) });
|
||||||
|
assert_eq!(2, unsafe { *(pixels.add(63)) });
|
||||||
|
|
||||||
|
let pixels = unsafe { bmp.pixels_at_ptr(1, 1).unwrap() };
|
||||||
|
assert_eq!(1, unsafe { *pixels });
|
||||||
|
assert_eq!(2, unsafe { *(pixels.add(54)) });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn pixels_at_mut_ptr() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
|
||||||
|
|
||||||
|
assert_eq!(None, unsafe { bmp.pixels_at_mut_ptr(-1, -1) });
|
||||||
|
|
||||||
|
let offset = bmp.get_offset_to_xy(1, 1);
|
||||||
|
let pixels = unsafe { bmp.pixels_at_mut_ptr(0, 0).unwrap() };
|
||||||
|
assert_eq!(0, unsafe { *pixels });
|
||||||
|
assert_eq!(1, unsafe { *(pixels.add(offset)) });
|
||||||
|
assert_eq!(2, unsafe { *(pixels.add(63)) });
|
||||||
|
|
||||||
|
let pixels = unsafe { bmp.pixels_at_mut_ptr(1, 1).unwrap() };
|
||||||
|
assert_eq!(1, unsafe { *pixels });
|
||||||
|
assert_eq!(2, unsafe { *(pixels.add(54)) });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn pixels_at_ptr_unchecked() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
|
||||||
|
|
||||||
|
let offset = bmp.get_offset_to_xy(1, 1);
|
||||||
|
let pixels = unsafe { bmp.pixels_at_ptr_unchecked(0, 0) };
|
||||||
|
assert_eq!(0, unsafe { *pixels });
|
||||||
|
assert_eq!(1, unsafe { *(pixels.add(offset)) });
|
||||||
|
assert_eq!(2, unsafe { *(pixels.add(63)) });
|
||||||
|
|
||||||
|
let pixels = unsafe { bmp.pixels_at_ptr_unchecked(1, 1) };
|
||||||
|
assert_eq!(1, unsafe { *pixels });
|
||||||
|
assert_eq!(2, unsafe { *(pixels.add(54)) });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn pixels_at_mut_ptr_unchecked() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
bmp.pixels_mut().copy_from_slice(RAW_BMP_PIXELS);
|
||||||
|
|
||||||
|
let offset = bmp.get_offset_to_xy(1, 1);
|
||||||
|
let pixels = unsafe { bmp.pixels_at_mut_ptr_unchecked(0, 0) };
|
||||||
|
assert_eq!(0, unsafe { *pixels });
|
||||||
|
assert_eq!(1, unsafe { *(pixels.add(offset)) });
|
||||||
|
assert_eq!(2, unsafe { *(pixels.add(63)) });
|
||||||
|
|
||||||
|
let pixels = unsafe { bmp.pixels_at_mut_ptr_unchecked(1, 1) };
|
||||||
|
assert_eq!(1, unsafe { *pixels });
|
||||||
|
assert_eq!(2, unsafe { *(pixels.add(54)) });
|
||||||
|
}
|
||||||
|
}
|
322
libretrogd/src/graphics/bitmap/pcx.rs
Normal file
322
libretrogd/src/graphics/bitmap/pcx.rs
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, BufWriter, Cursor, Seek, SeekFrom};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::utils::bytes::ReadFixedLengthByteArray;
|
||||||
|
use crate::{from_rgb32, Bitmap, Palette, PaletteError, PaletteFormat};
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum PcxError {
|
||||||
|
#[error("Bad or unsupported PCX file: {0}")]
|
||||||
|
BadFile(String),
|
||||||
|
|
||||||
|
#[error("PCX palette data error")]
|
||||||
|
BadPalette(#[from] PaletteError),
|
||||||
|
|
||||||
|
#[error("PCX I/O error")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
#[repr(packed)]
|
||||||
|
struct PcxHeader {
|
||||||
|
manufacturer: u8,
|
||||||
|
version: u8,
|
||||||
|
encoding: u8,
|
||||||
|
bpp: u8,
|
||||||
|
x1: u16,
|
||||||
|
y1: u16,
|
||||||
|
x2: u16,
|
||||||
|
y2: u16,
|
||||||
|
horizontal_dpi: u16,
|
||||||
|
vertical_dpi: u16,
|
||||||
|
ega_palette: [u8; 48],
|
||||||
|
reserved: u8,
|
||||||
|
num_color_planes: u8,
|
||||||
|
bytes_per_line: u16,
|
||||||
|
palette_type: u16,
|
||||||
|
horizontal_size: u16,
|
||||||
|
vertical_size: u16,
|
||||||
|
padding: [u8; 54],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PcxHeader {
|
||||||
|
pub fn read<T: ReadBytesExt>(reader: &mut T) -> Result<Self, PcxError> {
|
||||||
|
Ok(PcxHeader {
|
||||||
|
manufacturer: reader.read_u8()?,
|
||||||
|
version: reader.read_u8()?,
|
||||||
|
encoding: reader.read_u8()?,
|
||||||
|
bpp: reader.read_u8()?,
|
||||||
|
x1: reader.read_u16::<LittleEndian>()?,
|
||||||
|
y1: reader.read_u16::<LittleEndian>()?,
|
||||||
|
x2: reader.read_u16::<LittleEndian>()?,
|
||||||
|
y2: reader.read_u16::<LittleEndian>()?,
|
||||||
|
horizontal_dpi: reader.read_u16::<LittleEndian>()?,
|
||||||
|
vertical_dpi: reader.read_u16::<LittleEndian>()?,
|
||||||
|
ega_palette: reader.read_bytes()?,
|
||||||
|
reserved: reader.read_u8()?,
|
||||||
|
num_color_planes: reader.read_u8()?,
|
||||||
|
bytes_per_line: reader.read_u16::<LittleEndian>()?,
|
||||||
|
palette_type: reader.read_u16::<LittleEndian>()?,
|
||||||
|
horizontal_size: reader.read_u16::<LittleEndian>()?,
|
||||||
|
vertical_size: reader.read_u16::<LittleEndian>()?,
|
||||||
|
padding: reader.read_bytes()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn write<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), PcxError> {
|
||||||
|
writer.write_u8(self.manufacturer)?;
|
||||||
|
writer.write_u8(self.version)?;
|
||||||
|
writer.write_u8(self.encoding)?;
|
||||||
|
writer.write_u8(self.bpp)?;
|
||||||
|
writer.write_u16::<LittleEndian>(self.x1)?;
|
||||||
|
writer.write_u16::<LittleEndian>(self.y1)?;
|
||||||
|
writer.write_u16::<LittleEndian>(self.x2)?;
|
||||||
|
writer.write_u16::<LittleEndian>(self.y2)?;
|
||||||
|
writer.write_u16::<LittleEndian>(self.horizontal_dpi)?;
|
||||||
|
writer.write_u16::<LittleEndian>(self.vertical_dpi)?;
|
||||||
|
writer.write_all(&self.ega_palette)?;
|
||||||
|
writer.write_u8(self.reserved)?;
|
||||||
|
writer.write_u8(self.num_color_planes)?;
|
||||||
|
writer.write_u16::<LittleEndian>(self.bytes_per_line)?;
|
||||||
|
writer.write_u16::<LittleEndian>(self.palette_type)?;
|
||||||
|
writer.write_u16::<LittleEndian>(self.horizontal_size)?;
|
||||||
|
writer.write_u16::<LittleEndian>(self.vertical_size)?;
|
||||||
|
writer.write_all(&self.padding)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_pcx_data<T: WriteBytesExt>(
|
||||||
|
writer: &mut T,
|
||||||
|
run_count: u8,
|
||||||
|
pixel: u8,
|
||||||
|
) -> Result<(), PcxError> {
|
||||||
|
if (run_count > 1) || ((pixel & 0xc0) == 0xc0) {
|
||||||
|
writer.write_u8(0xc0 | run_count)?;
|
||||||
|
}
|
||||||
|
writer.write_u8(pixel)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bitmap {
|
||||||
|
pub fn load_pcx_bytes<T: ReadBytesExt + Seek>(
|
||||||
|
reader: &mut T,
|
||||||
|
) -> Result<(Bitmap, Palette), PcxError> {
|
||||||
|
let header = PcxHeader::read(reader)?;
|
||||||
|
|
||||||
|
if header.manufacturer != 10 {
|
||||||
|
return Err(PcxError::BadFile(String::from(
|
||||||
|
"Unexpected header.manufacturer value, probably not a PCX file",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if header.version != 5 {
|
||||||
|
return Err(PcxError::BadFile(String::from(
|
||||||
|
"Only version 5 PCX files are supported",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if header.encoding != 1 {
|
||||||
|
return Err(PcxError::BadFile(String::from(
|
||||||
|
"Only RLE-compressed PCX files are supported",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if header.bpp != 8 {
|
||||||
|
return Err(PcxError::BadFile(String::from(
|
||||||
|
"Only 8-bit indexed (256 color palette) PCX files are supported",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
if header.x2 == 0 || header.y2 == 0 {
|
||||||
|
return Err(PcxError::BadFile(String::from(
|
||||||
|
"Invalid PCX image dimensions",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// read the PCX file's pixel data into a bitmap
|
||||||
|
|
||||||
|
let width = (header.x2 + 1) as u32;
|
||||||
|
let height = (header.y2 + 1) as u32;
|
||||||
|
let mut bmp = Bitmap::new(width, height).unwrap();
|
||||||
|
let mut writer = Cursor::new(bmp.pixels_mut());
|
||||||
|
|
||||||
|
for _y in 0..height {
|
||||||
|
// read the next scanline's worth of pixels from the PCX file
|
||||||
|
let mut x: u32 = 0;
|
||||||
|
while x < (header.bytes_per_line as u32) {
|
||||||
|
let mut data: u8;
|
||||||
|
let mut count: u32;
|
||||||
|
|
||||||
|
// read pixel or RLE count
|
||||||
|
data = reader.read_u8()?;
|
||||||
|
|
||||||
|
if (data & 0xc0) == 0xc0 {
|
||||||
|
// it was an RLE count, actual pixel is the next byte ...
|
||||||
|
count = (data & 0x3f) as u32;
|
||||||
|
data = reader.read_u8()?;
|
||||||
|
} else {
|
||||||
|
// it was just a single pixel
|
||||||
|
count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write the current pixel value 'data' to the bitmap 'count' number of times
|
||||||
|
while count > 0 {
|
||||||
|
if x <= width {
|
||||||
|
writer.write_u8(data)?;
|
||||||
|
} else {
|
||||||
|
writer.seek(SeekFrom::Current(1))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
x += 1;
|
||||||
|
count -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// now read the palette data located at the end of the PCX file
|
||||||
|
// palette data should be for 256 colors, 3 bytes per color = 768 bytes
|
||||||
|
// the palette is preceded by a single byte, 0x0c, which we will also validate
|
||||||
|
|
||||||
|
reader.seek(SeekFrom::End(-769))?;
|
||||||
|
|
||||||
|
let palette_marker = reader.read_u8()?;
|
||||||
|
if palette_marker != 0x0c {
|
||||||
|
return Err(PcxError::BadFile(String::from(
|
||||||
|
"Palette not found at end of file",
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
let palette = Palette::load_from_bytes(reader, PaletteFormat::Normal)?;
|
||||||
|
|
||||||
|
Ok((bmp, palette))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_pcx_file(path: &Path) -> Result<(Bitmap, Palette), PcxError> {
|
||||||
|
let f = File::open(path)?;
|
||||||
|
let mut reader = BufReader::new(f);
|
||||||
|
Self::load_pcx_bytes(&mut reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_pcx_bytes<T: WriteBytesExt>(
|
||||||
|
&self,
|
||||||
|
writer: &mut T,
|
||||||
|
palette: &Palette,
|
||||||
|
) -> Result<(), PcxError> {
|
||||||
|
let header = PcxHeader {
|
||||||
|
manufacturer: 10,
|
||||||
|
version: 5,
|
||||||
|
encoding: 1,
|
||||||
|
bpp: 8,
|
||||||
|
x1: 0,
|
||||||
|
y1: 0,
|
||||||
|
x2: self.right() as u16,
|
||||||
|
y2: self.bottom() as u16,
|
||||||
|
horizontal_dpi: 320,
|
||||||
|
vertical_dpi: 200,
|
||||||
|
ega_palette: [0u8; 48],
|
||||||
|
reserved: 0,
|
||||||
|
num_color_planes: 1,
|
||||||
|
bytes_per_line: self.width() as u16,
|
||||||
|
palette_type: 1,
|
||||||
|
horizontal_size: self.width() as u16,
|
||||||
|
vertical_size: self.height() as u16,
|
||||||
|
padding: [0u8; 54],
|
||||||
|
};
|
||||||
|
header.write(writer)?;
|
||||||
|
|
||||||
|
let pixels = self.pixels();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
for _y in 0..=self.bottom() {
|
||||||
|
// write one scanline at a time. breaking runs that could have continued across
|
||||||
|
// scanlines in the process, as per the pcx standard
|
||||||
|
|
||||||
|
let mut run_count = 0;
|
||||||
|
let mut run_pixel = 0;
|
||||||
|
|
||||||
|
for _x in 0..=self.right() {
|
||||||
|
let pixel = pixels[i];
|
||||||
|
i += 1;
|
||||||
|
|
||||||
|
if run_count == 0 {
|
||||||
|
run_count = 1;
|
||||||
|
run_pixel = pixel;
|
||||||
|
} else {
|
||||||
|
if (pixel != run_pixel) || (run_count >= 63) {
|
||||||
|
write_pcx_data(writer, run_count, run_pixel)?;
|
||||||
|
run_count = 1;
|
||||||
|
run_pixel = pixel;
|
||||||
|
} else {
|
||||||
|
run_count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// end the scanline, writing out whatever run we might have had going
|
||||||
|
write_pcx_data(writer, run_count, run_pixel)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// marker for beginning of palette data
|
||||||
|
writer.write_u8(0xc)?;
|
||||||
|
|
||||||
|
for i in 0..=255 {
|
||||||
|
let argb = palette[i];
|
||||||
|
let (r, g, b) = from_rgb32(argb);
|
||||||
|
writer.write_u8(r)?;
|
||||||
|
writer.write_u8(g)?;
|
||||||
|
writer.write_u8(b)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_pcx_file(&self, path: &Path, palette: &Palette) -> Result<(), PcxError> {
|
||||||
|
let f = File::create(path)?;
|
||||||
|
let mut writer = BufWriter::new(f);
|
||||||
|
self.to_pcx_bytes(&mut writer, palette)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub static TEST_BMP_PIXELS_RAW: &[u8] =
|
||||||
|
include_bytes!("../../../test-assets/test_bmp_pixels_raw.bin");
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn load_and_save() -> Result<(), PcxError> {
|
||||||
|
let dp2_palette =
|
||||||
|
Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal)
|
||||||
|
.unwrap();
|
||||||
|
let tmp_dir = TempDir::new()?;
|
||||||
|
|
||||||
|
let (bmp, palette) = Bitmap::load_pcx_file(Path::new("./test-assets/test.pcx"))?;
|
||||||
|
assert_eq!(16, bmp.width());
|
||||||
|
assert_eq!(16, bmp.height());
|
||||||
|
assert_eq!(bmp.pixels(), TEST_BMP_PIXELS_RAW);
|
||||||
|
assert_eq!(palette, dp2_palette);
|
||||||
|
|
||||||
|
let save_path = tmp_dir.path().join("test_save.pcx");
|
||||||
|
bmp.to_pcx_file(&save_path, &palette)?;
|
||||||
|
let (reloaded_bmp, reloaded_palette) = Bitmap::load_pcx_file(&save_path)?;
|
||||||
|
assert_eq!(16, reloaded_bmp.width());
|
||||||
|
assert_eq!(16, reloaded_bmp.height());
|
||||||
|
assert_eq!(reloaded_bmp.pixels(), TEST_BMP_PIXELS_RAW);
|
||||||
|
assert_eq!(reloaded_palette, dp2_palette);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn load_larger_image() -> Result<(), PcxError> {
|
||||||
|
let (bmp, _palette) = Bitmap::load_pcx_file(Path::new("./test-assets/test_image.pcx"))?;
|
||||||
|
assert_eq!(320, bmp.width());
|
||||||
|
assert_eq!(200, bmp.height());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
378
libretrogd/src/graphics/bitmap/primitives.rs
Normal file
378
libretrogd/src/graphics/bitmap/primitives.rs
Normal file
|
@ -0,0 +1,378 @@
|
||||||
|
use std::mem::swap;
|
||||||
|
|
||||||
|
use crate::{Bitmap, Character, Font, FontRenderOpts, Rect};
|
||||||
|
|
||||||
|
impl Bitmap {
|
||||||
|
/// Fills the entire bitmap with the given color.
|
||||||
|
pub fn clear(&mut self, color: u8) {
|
||||||
|
self.pixels.fill(color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the pixel at the given coordinates to the color specified. If the coordinates lie
|
||||||
|
/// outside of the bitmaps clipping region, no pixels will be changed.
|
||||||
|
#[inline]
|
||||||
|
pub fn set_pixel(&mut self, x: i32, y: i32, color: u8) {
|
||||||
|
if let Some(pixels) = self.pixels_at_mut(x, y) {
|
||||||
|
pixels[0] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the pixel at the given coordinates to the color specified. The coordinates are not
|
||||||
|
/// checked for validity, so it is up to you to ensure they lie within the bounds of the
|
||||||
|
/// bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn set_pixel_unchecked(&mut self, x: i32, y: i32, color: u8) {
|
||||||
|
let p = self.pixels_at_mut_ptr_unchecked(x, y);
|
||||||
|
*p = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the pixel at the given coordinates. If the coordinates lie outside of the bitmaps
|
||||||
|
/// clipping region, None is returned.
|
||||||
|
#[inline]
|
||||||
|
pub fn get_pixel(&self, x: i32, y: i32) -> Option<u8> {
|
||||||
|
if let Some(pixels) = self.pixels_at(x, y) {
|
||||||
|
Some(pixels[0])
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the pixel at the given coordinates. The coordinates are not checked for validity, so
|
||||||
|
/// it is up to you to ensure they lie within the bounds of the bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub unsafe fn get_pixel_unchecked(&self, x: i32, y: i32) -> u8 {
|
||||||
|
*(self.pixels_at_ptr_unchecked(x, y))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders a single character using the font given.
|
||||||
|
#[inline]
|
||||||
|
pub fn print_char<T: Font>(&mut self, ch: char, x: i32, y: i32, color: u8, font: &T) {
|
||||||
|
font.character(ch)
|
||||||
|
.draw(self, x, y, FontRenderOpts::Color(color));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the string of text using the font given.
|
||||||
|
pub fn print_string<T: Font>(&mut self, text: &str, x: i32, y: i32, color: u8, font: &T) {
|
||||||
|
let mut current_x = x;
|
||||||
|
let mut current_y = y;
|
||||||
|
for ch in text.chars() {
|
||||||
|
match ch {
|
||||||
|
' ' => current_x += font.space_width() as i32,
|
||||||
|
'\n' => {
|
||||||
|
current_x = x;
|
||||||
|
current_y += font.line_height() as i32
|
||||||
|
}
|
||||||
|
'\r' => (),
|
||||||
|
otherwise => {
|
||||||
|
self.print_char(otherwise, current_x, current_y, color, font);
|
||||||
|
current_x += font.character(otherwise).bounds().width as i32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a line from x1,y1 to x2,y2.
|
||||||
|
pub fn line(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) {
|
||||||
|
let mut dx = x1;
|
||||||
|
let mut dy = y1;
|
||||||
|
let delta_x = x2 - x1;
|
||||||
|
let delta_y = y2 - y1;
|
||||||
|
let delta_x_abs = delta_x.abs();
|
||||||
|
let delta_y_abs = delta_y.abs();
|
||||||
|
let delta_x_sign = delta_x.signum();
|
||||||
|
let delta_y_sign = delta_y.signum();
|
||||||
|
let mut x = delta_x_abs / 2;
|
||||||
|
let mut y = delta_y_abs / 2;
|
||||||
|
let offset_x_inc = delta_x_sign;
|
||||||
|
let offset_y_inc = delta_y_sign * self.width as i32;
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
// safety: while we are blindly getting a pointer to this x/y coordinate, we don't
|
||||||
|
// write to it unless we know the coordinates are in bounds.
|
||||||
|
// TODO: should be ok ... ? or am i making too many assumptions about memory layout?
|
||||||
|
let mut dest = self.pixels_at_mut_ptr_unchecked(x1, y1);
|
||||||
|
|
||||||
|
if self.is_xy_visible(dx, dy) {
|
||||||
|
*dest = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
if delta_x_abs >= delta_y_abs {
|
||||||
|
for _ in 0..delta_x_abs {
|
||||||
|
y += delta_y_abs;
|
||||||
|
|
||||||
|
if y >= delta_x_abs {
|
||||||
|
y -= delta_x_abs;
|
||||||
|
dy += delta_y_sign;
|
||||||
|
dest = dest.offset(offset_y_inc as isize);
|
||||||
|
}
|
||||||
|
|
||||||
|
dx += delta_x_sign;
|
||||||
|
dest = dest.offset(offset_x_inc as isize);
|
||||||
|
|
||||||
|
if self.is_xy_visible(dx, dy) {
|
||||||
|
*dest = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _ in 0..delta_y_abs {
|
||||||
|
x += delta_x_abs;
|
||||||
|
|
||||||
|
if x >= delta_y_abs {
|
||||||
|
x -= delta_y_abs;
|
||||||
|
dx += delta_x_sign;
|
||||||
|
dest = dest.offset(offset_x_inc as isize);
|
||||||
|
}
|
||||||
|
|
||||||
|
dy += delta_y_sign;
|
||||||
|
dest = dest.offset(offset_y_inc as isize);
|
||||||
|
|
||||||
|
if self.is_xy_visible(dx, dy) {
|
||||||
|
*dest = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a horizontal line from x1,y to x2,y.
|
||||||
|
pub fn horiz_line(&mut self, x1: i32, x2: i32, y: i32, color: u8) {
|
||||||
|
let mut region = Rect::from_coords(x1, y, x2, y);
|
||||||
|
if region.clamp_to(&self.clip_region) {
|
||||||
|
unsafe {
|
||||||
|
let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y);
|
||||||
|
dest.write_bytes(color, region.width as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a vertical line from x,y1 to x,y2.
|
||||||
|
pub fn vert_line(&mut self, x: i32, y1: i32, y2: i32, color: u8) {
|
||||||
|
let mut region = Rect::from_coords(x, y1, x, y2);
|
||||||
|
if region.clamp_to(&self.clip_region) {
|
||||||
|
unsafe {
|
||||||
|
let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y);
|
||||||
|
for _ in 0..region.height {
|
||||||
|
*dest = color;
|
||||||
|
dest = dest.add(self.width as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws an empty box (rectangle) using the points x1,y1 and x2,y2 to form the box to be
|
||||||
|
/// drawn, assuming they are specifying the top-left and bottom-right corners respectively.
|
||||||
|
pub fn rect(&mut self, mut x1: i32, mut y1: i32, mut x2: i32, mut y2: i32, color: u8) {
|
||||||
|
// note: need to manually do all this instead of just relying on Rect::from_coords (which
|
||||||
|
// could otherwise figure all this out for us) mainly just because we need the post-swap
|
||||||
|
// x1,y1,x2,y2 values for post-region-clamping comparison purposes ...
|
||||||
|
if x2 < x1 {
|
||||||
|
swap(&mut x1, &mut x2);
|
||||||
|
}
|
||||||
|
if y2 < y1 {
|
||||||
|
swap(&mut y1, &mut y2);
|
||||||
|
}
|
||||||
|
let mut region = Rect {
|
||||||
|
x: x1,
|
||||||
|
y: y1,
|
||||||
|
width: (x2 - x1 + 1) as u32,
|
||||||
|
height: (y2 - y1 + 1) as u32,
|
||||||
|
};
|
||||||
|
if !region.clamp_to(&self.clip_region) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// top line, only if y1 was originally within bounds
|
||||||
|
if y1 == region.y {
|
||||||
|
unsafe {
|
||||||
|
let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y);
|
||||||
|
dest.write_bytes(color, region.width as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bottom line, only if y2 was originally within bounds
|
||||||
|
if y2 == region.bottom() {
|
||||||
|
unsafe {
|
||||||
|
let dest = self.pixels_at_mut_ptr_unchecked(region.x, region.bottom());
|
||||||
|
dest.write_bytes(color, region.width as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// left line, only if x1 was originally within bounds
|
||||||
|
if x1 == region.x {
|
||||||
|
unsafe {
|
||||||
|
let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y);
|
||||||
|
for _ in 0..region.height {
|
||||||
|
*dest = color;
|
||||||
|
dest = dest.add(self.width as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// right line, only if x2 was originally within bounds
|
||||||
|
if x2 == region.right() {
|
||||||
|
unsafe {
|
||||||
|
let mut dest = self.pixels_at_mut_ptr_unchecked(region.right(), region.y);
|
||||||
|
for _ in 0..region.height {
|
||||||
|
*dest = color;
|
||||||
|
dest = dest.add(self.width as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a filled box (rectangle) using the points x1,y1 and x2,y2 to form the box to be
|
||||||
|
/// drawn, assuming they are specifying the top-left and bottom-right corners respectively.
|
||||||
|
pub fn filled_rect(&mut self, x1: i32, y1: i32, x2: i32, y2: i32, color: u8) {
|
||||||
|
let mut region = Rect::from_coords(x1, y1, x2, y2);
|
||||||
|
if region.clamp_to(&self.clip_region) {
|
||||||
|
unsafe {
|
||||||
|
let mut dest = self.pixels_at_mut_ptr_unchecked(region.x, region.y);
|
||||||
|
for _ in 0..region.height {
|
||||||
|
dest.write_bytes(color, region.width as usize);
|
||||||
|
dest = dest.add(self.width as usize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws the outline of a circle formed by the center point and radius given.
|
||||||
|
pub fn circle(&mut self, center_x: i32, center_y: i32, radius: u32, color: u8) {
|
||||||
|
// TODO: optimize
|
||||||
|
let mut x = 0;
|
||||||
|
let mut y = radius as i32;
|
||||||
|
let mut m = 5 - 4 * radius as i32;
|
||||||
|
|
||||||
|
while x <= y {
|
||||||
|
self.set_pixel(center_x + x, center_y + y, color);
|
||||||
|
self.set_pixel(center_x + x, center_y - y, color);
|
||||||
|
self.set_pixel(center_x - x, center_y + y, color);
|
||||||
|
self.set_pixel(center_x - x, center_y - y, color);
|
||||||
|
self.set_pixel(center_x + y, center_y + x, color);
|
||||||
|
self.set_pixel(center_x + y, center_y - x, color);
|
||||||
|
self.set_pixel(center_x - y, center_y + x, color);
|
||||||
|
self.set_pixel(center_x - y, center_y - x, color);
|
||||||
|
|
||||||
|
if m > 0 {
|
||||||
|
y -= 1;
|
||||||
|
m -= 8 * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
x += 1;
|
||||||
|
m += 8 * x + 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws a filled circle formed by the center point and radius given.
|
||||||
|
pub fn filled_circle(&mut self, center_x: i32, center_y: i32, radius: u32, color: u8) {
|
||||||
|
// TODO: optimize
|
||||||
|
let mut x = 0;
|
||||||
|
let mut y = radius as i32;
|
||||||
|
let mut m = 5 - 4 * radius as i32;
|
||||||
|
|
||||||
|
while x <= y {
|
||||||
|
self.horiz_line(center_x - x, center_x + x, center_y - y, color);
|
||||||
|
self.horiz_line(center_x - y, center_x + y, center_y - x, color);
|
||||||
|
self.horiz_line(center_x - y, center_x + y, center_y + x, color);
|
||||||
|
self.horiz_line(center_x - x, center_x + x, center_y + y, color);
|
||||||
|
|
||||||
|
if m > 0 {
|
||||||
|
y -= 1;
|
||||||
|
m -= 8 * y;
|
||||||
|
}
|
||||||
|
|
||||||
|
x += 1;
|
||||||
|
m += 8 * x + 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[test]
|
||||||
|
pub fn set_and_get_pixel() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(None, bmp.get_pixel(-1, -1));
|
||||||
|
|
||||||
|
assert_eq!(0, bmp.get_pixel(0, 0).unwrap());
|
||||||
|
bmp.set_pixel(0, 0, 7);
|
||||||
|
assert_eq!(7, bmp.get_pixel(0, 0).unwrap());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
bmp.pixels(),
|
||||||
|
&[
|
||||||
|
7, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(0, bmp.get_pixel(2, 4).unwrap());
|
||||||
|
bmp.set_pixel(2, 4, 5);
|
||||||
|
assert_eq!(5, bmp.get_pixel(2, 4).unwrap());
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
bmp.pixels(),
|
||||||
|
&[
|
||||||
|
7, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 5, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[test]
|
||||||
|
pub fn set_and_get_pixel_unchecked() {
|
||||||
|
let mut bmp = Bitmap::new(8, 8).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(0, unsafe { bmp.get_pixel_unchecked(0, 0) });
|
||||||
|
unsafe { bmp.set_pixel_unchecked(0, 0, 7) };
|
||||||
|
assert_eq!(7, unsafe { bmp.get_pixel_unchecked(0, 0) });
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
bmp.pixels(),
|
||||||
|
&[
|
||||||
|
7, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(0, unsafe { bmp.get_pixel_unchecked(2, 4) });
|
||||||
|
unsafe { bmp.set_pixel_unchecked(2, 4, 5) };
|
||||||
|
assert_eq!(5, unsafe { bmp.get_pixel_unchecked(2, 4) });
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
bmp.pixels(),
|
||||||
|
&[
|
||||||
|
7, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 5, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
178
libretrogd/src/graphics/bitmapatlas.rs
Normal file
178
libretrogd/src/graphics/bitmapatlas.rs
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
use std::ops::Index;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::{Bitmap, Rect};
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum BitmapAtlasError {
|
||||||
|
#[error("Region is out of bounds for the Bitmap used by the BitmapAtlas")]
|
||||||
|
OutOfBounds,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BitmapAtlas {
|
||||||
|
bitmap: Bitmap,
|
||||||
|
bounds: Rect,
|
||||||
|
tiles: Vec<Rect>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BitmapAtlas {
|
||||||
|
pub fn new(bitmap: Bitmap) -> BitmapAtlas {
|
||||||
|
let bounds = bitmap.full_bounds();
|
||||||
|
BitmapAtlas {
|
||||||
|
bitmap,
|
||||||
|
bounds,
|
||||||
|
tiles: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, rect: Rect) -> Result<usize, BitmapAtlasError> {
|
||||||
|
if !self.bounds.contains_rect(&rect) {
|
||||||
|
return Err(BitmapAtlasError::OutOfBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tiles.push(rect);
|
||||||
|
Ok(self.tiles.len() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_grid(
|
||||||
|
&mut self,
|
||||||
|
tile_width: u32,
|
||||||
|
tile_height: u32,
|
||||||
|
) -> Result<usize, BitmapAtlasError> {
|
||||||
|
if self.bounds.width < tile_width || self.bounds.height < tile_height {
|
||||||
|
return Err(BitmapAtlasError::OutOfBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
for yt in 0..(self.bounds.height / tile_height) {
|
||||||
|
for xt in 0..(self.bounds.width) / tile_width {
|
||||||
|
let x = xt * tile_width;
|
||||||
|
let y = yt * tile_height;
|
||||||
|
let rect = Rect::new(x as i32, y as i32, tile_width, tile_height);
|
||||||
|
self.tiles.push(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.tiles.len() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_custom_grid(
|
||||||
|
&mut self,
|
||||||
|
start_x: u32,
|
||||||
|
start_y: u32,
|
||||||
|
tile_width: u32,
|
||||||
|
tile_height: u32,
|
||||||
|
x_tiles: u32,
|
||||||
|
y_tiles: u32,
|
||||||
|
border: u32,
|
||||||
|
) -> Result<usize, BitmapAtlasError> {
|
||||||
|
// figure out of the grid properties given would result in us creating any
|
||||||
|
// rects that lie out of the bounds of this bitmap
|
||||||
|
let grid_region = Rect::new(
|
||||||
|
start_x as i32,
|
||||||
|
start_y as i32,
|
||||||
|
(tile_width + border) * x_tiles + border,
|
||||||
|
(tile_height + border) * y_tiles + border,
|
||||||
|
);
|
||||||
|
if !self.bounds.contains_rect(&grid_region) {
|
||||||
|
return Err(BitmapAtlasError::OutOfBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// all good! now create all the tiles needed for the grid specified
|
||||||
|
for yt in 0..y_tiles {
|
||||||
|
for xt in 0..x_tiles {
|
||||||
|
let x = start_x + (tile_width + border) * xt;
|
||||||
|
let y = start_y + (tile_height + border) * yt;
|
||||||
|
let rect = Rect::new(x as i32, y as i32, tile_width, tile_height);
|
||||||
|
self.tiles.push(rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(self.tiles.len() - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.tiles.clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
|
self.tiles.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bitmap(&self) -> &Bitmap {
|
||||||
|
&self.bitmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<usize> for BitmapAtlas {
|
||||||
|
type Output = Rect;
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> &Self::Output {
|
||||||
|
&self.tiles[index]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use claim::assert_matches;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn adding_rects() {
|
||||||
|
let bmp = Bitmap::new(64, 64).unwrap();
|
||||||
|
let mut atlas = BitmapAtlas::new(bmp);
|
||||||
|
|
||||||
|
let rect = Rect::new(0, 0, 16, 16);
|
||||||
|
assert_eq!(0, atlas.add(rect.clone()).unwrap());
|
||||||
|
assert_eq!(rect, atlas[0]);
|
||||||
|
assert_eq!(1, atlas.len());
|
||||||
|
|
||||||
|
let rect = Rect::new(16, 0, 16, 16);
|
||||||
|
assert_eq!(1, atlas.add(rect.clone()).unwrap());
|
||||||
|
assert_eq!(rect, atlas[1]);
|
||||||
|
assert_eq!(2, atlas.len());
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
atlas.add(Rect::new(56, 0, 16, 16)),
|
||||||
|
Err(BitmapAtlasError::OutOfBounds)
|
||||||
|
);
|
||||||
|
assert_eq!(2, atlas.len());
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
atlas.add(Rect::new(-8, 4, 16, 16)),
|
||||||
|
Err(BitmapAtlasError::OutOfBounds)
|
||||||
|
);
|
||||||
|
assert_eq!(2, atlas.len());
|
||||||
|
|
||||||
|
assert_matches!(
|
||||||
|
atlas.add(Rect::new(0, 0, 128, 128)),
|
||||||
|
Err(BitmapAtlasError::OutOfBounds)
|
||||||
|
);
|
||||||
|
assert_eq!(2, atlas.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn adding_grid() {
|
||||||
|
let bmp = Bitmap::new(64, 64).unwrap();
|
||||||
|
let mut atlas = BitmapAtlas::new(bmp);
|
||||||
|
|
||||||
|
assert_eq!(3, atlas.add_custom_grid(0, 0, 8, 8, 2, 2, 0).unwrap());
|
||||||
|
assert_eq!(4, atlas.len());
|
||||||
|
assert_eq!(Rect::new(0, 0, 8, 8), atlas[0]);
|
||||||
|
assert_eq!(Rect::new(8, 0, 8, 8), atlas[1]);
|
||||||
|
assert_eq!(Rect::new(0, 8, 8, 8), atlas[2]);
|
||||||
|
assert_eq!(Rect::new(8, 8, 8, 8), atlas[3]);
|
||||||
|
|
||||||
|
atlas.clear();
|
||||||
|
assert_eq!(0, atlas.len());
|
||||||
|
|
||||||
|
assert_eq!(3, atlas.add_custom_grid(0, 0, 4, 8, 2, 2, 1).unwrap());
|
||||||
|
assert_eq!(4, atlas.len());
|
||||||
|
assert_eq!(Rect::new(0, 0, 4, 8), atlas[0]);
|
||||||
|
assert_eq!(Rect::new(5, 0, 4, 8), atlas[1]);
|
||||||
|
assert_eq!(Rect::new(0, 9, 4, 8), atlas[2]);
|
||||||
|
assert_eq!(Rect::new(5, 9, 4, 8), atlas[3]);
|
||||||
|
}
|
||||||
|
}
|
202
libretrogd/src/graphics/font.rs
Normal file
202
libretrogd/src/graphics/font.rs
Normal file
|
@ -0,0 +1,202 @@
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, BufWriter, Cursor};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::{Bitmap, Rect};
|
||||||
|
|
||||||
|
pub static VGA_FONT_BYTES: &[u8] = include_bytes!("../../assets/vga.fnt");
|
||||||
|
|
||||||
|
pub const NUM_CHARS: usize = 256;
|
||||||
|
pub const CHAR_HEIGHT: usize = 8;
|
||||||
|
pub const CHAR_FIXED_WIDTH: usize = 8;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum FontError {
|
||||||
|
#[error("Invalid font file: {0}")]
|
||||||
|
InvalidFile(String),
|
||||||
|
|
||||||
|
#[error("Font I/O error")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum FontRenderOpts {
|
||||||
|
Color(u8),
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Character {
|
||||||
|
fn bounds(&self) -> &Rect;
|
||||||
|
fn draw(&self, dest: &mut Bitmap, x: i32, y: i32, opts: FontRenderOpts);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Font {
|
||||||
|
type CharacterType: Character;
|
||||||
|
|
||||||
|
fn character(&self, ch: char) -> &Self::CharacterType;
|
||||||
|
fn space_width(&self) -> u8;
|
||||||
|
fn line_height(&self) -> u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BitmaskCharacter {
|
||||||
|
bytes: [u8; CHAR_HEIGHT],
|
||||||
|
bounds: Rect,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Character for BitmaskCharacter {
|
||||||
|
#[inline]
|
||||||
|
fn bounds(&self) -> &Rect {
|
||||||
|
&self.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw(&self, dest: &mut Bitmap, x: i32, y: i32, opts: FontRenderOpts) {
|
||||||
|
// out of bounds check
|
||||||
|
if ((x + self.bounds.width as i32) < dest.clip_region().x)
|
||||||
|
|| ((y + self.bounds.height as i32) < dest.clip_region().y)
|
||||||
|
|| (x >= dest.clip_region().right())
|
||||||
|
|| (y >= dest.clip_region().bottom())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = match opts {
|
||||||
|
FontRenderOpts::Color(color) => color,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: i'm sure this can be optimized, lol
|
||||||
|
for char_y in 0..self.bounds.height as usize {
|
||||||
|
let mut bit_mask = 0x80;
|
||||||
|
for char_x in 0..self.bounds.width as usize {
|
||||||
|
if self.bytes[char_y] & bit_mask > 0 {
|
||||||
|
dest.set_pixel(x + char_x as i32, y + char_y as i32, color);
|
||||||
|
}
|
||||||
|
bit_mask >>= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct BitmaskFont {
|
||||||
|
characters: Box<[BitmaskCharacter]>,
|
||||||
|
line_height: u8,
|
||||||
|
space_width: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BitmaskFont {
|
||||||
|
pub fn new_vga_font() -> Result<BitmaskFont, FontError> {
|
||||||
|
BitmaskFont::load_from_bytes(&mut Cursor::new(VGA_FONT_BYTES))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_from_file(path: &Path) -> Result<BitmaskFont, FontError> {
|
||||||
|
let f = File::open(path)?;
|
||||||
|
let mut reader = BufReader::new(f);
|
||||||
|
|
||||||
|
BitmaskFont::load_from_bytes(&mut reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_from_bytes<T: ReadBytesExt>(reader: &mut T) -> Result<BitmaskFont, FontError> {
|
||||||
|
let mut characters: Vec<BitmaskCharacter> = Vec::with_capacity(NUM_CHARS);
|
||||||
|
|
||||||
|
// read character bitmap data
|
||||||
|
for _ in 0..NUM_CHARS {
|
||||||
|
let mut buffer = [0u8; CHAR_HEIGHT];
|
||||||
|
reader.read_exact(&mut buffer)?;
|
||||||
|
let character = BitmaskCharacter {
|
||||||
|
bytes: buffer,
|
||||||
|
// bounds are filled in below. ugh.
|
||||||
|
bounds: Rect {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
characters.push(character);
|
||||||
|
}
|
||||||
|
|
||||||
|
// read character widths (used for rendering)
|
||||||
|
for i in 0..NUM_CHARS {
|
||||||
|
characters[i].bounds.width = reader.read_u8()? as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read global font height (used for rendering)
|
||||||
|
let line_height = reader.read_u8()?;
|
||||||
|
for i in 0..NUM_CHARS {
|
||||||
|
characters[i].bounds.height = line_height as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
let space_width = characters[' ' as usize].bounds.width as u8;
|
||||||
|
|
||||||
|
Ok(BitmaskFont {
|
||||||
|
characters: characters.into_boxed_slice(),
|
||||||
|
line_height,
|
||||||
|
space_width,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_file(&self, path: &Path) -> Result<(), FontError> {
|
||||||
|
let f = File::create(path)?;
|
||||||
|
let mut writer = BufWriter::new(f);
|
||||||
|
self.to_bytes(&mut writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_bytes<T: WriteBytesExt>(&self, writer: &mut T) -> Result<(), FontError> {
|
||||||
|
// write character bitmap data
|
||||||
|
for i in 0..NUM_CHARS {
|
||||||
|
writer.write_all(&self.characters[i].bytes)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write character widths
|
||||||
|
for i in 0..NUM_CHARS {
|
||||||
|
writer.write_u8(self.characters[i].bounds.width as u8)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// write global font height
|
||||||
|
writer.write_u8(self.line_height)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Font for BitmaskFont {
|
||||||
|
type CharacterType = BitmaskCharacter;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn character(&self, ch: char) -> &Self::CharacterType {
|
||||||
|
&self.characters[ch as usize]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn space_width(&self) -> u8 {
|
||||||
|
self.space_width
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn line_height(&self) -> u8 {
|
||||||
|
self.line_height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn load_font() -> Result<(), FontError> {
|
||||||
|
let font = BitmaskFont::load_from_file(Path::new("./assets/vga.fnt"))?;
|
||||||
|
assert_eq!(256, font.characters.len());
|
||||||
|
assert_eq!(CHAR_FIXED_WIDTH as u8, font.space_width);
|
||||||
|
for character in font.characters.iter() {
|
||||||
|
assert_eq!(CHAR_FIXED_WIDTH as u8, character.bounds.width as u8);
|
||||||
|
assert_eq!(CHAR_HEIGHT, character.bytes.len());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
4
libretrogd/src/graphics/mod.rs
Normal file
4
libretrogd/src/graphics/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
pub mod bitmap;
|
||||||
|
pub mod bitmapatlas;
|
||||||
|
pub mod font;
|
||||||
|
pub mod palette;
|
563
libretrogd/src/graphics/palette.rs
Normal file
563
libretrogd/src/graphics/palette.rs
Normal file
|
@ -0,0 +1,563 @@
|
||||||
|
use std::cmp::min;
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{BufReader, BufWriter, Cursor};
|
||||||
|
use std::ops::{Bound, Index, IndexMut, RangeBounds};
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::utils::abs_diff;
|
||||||
|
use crate::NUM_COLORS;
|
||||||
|
|
||||||
|
// silly "hack" (???) which allows us to alias the generic constraint `RangeBounds<u8> + Iterator<Item = u8>` to `ColorRange`
|
||||||
|
pub trait ColorRange: RangeBounds<u8> + Iterator<Item = u8> {}
|
||||||
|
impl<T> ColorRange for T where T: RangeBounds<u8> + Iterator<Item = u8> {}
|
||||||
|
|
||||||
|
pub static VGA_PALETTE_BYTES: &[u8] = include_bytes!("../../assets/vga.pal");
|
||||||
|
|
||||||
|
/// Converts a set of individual ARGB components to a combined 32-bit color value, packed into
|
||||||
|
/// the format 0xAARRGGBB
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `a`: the alpha component (0-255)
|
||||||
|
/// * `r`: the red component (0-255)
|
||||||
|
/// * `g`: the green component (0-255)
|
||||||
|
/// * `b`: the blue component (0-255)
|
||||||
|
///
|
||||||
|
/// returns: the u32 packed color
|
||||||
|
#[inline]
|
||||||
|
pub fn to_argb32(a: u8, r: u8, g: u8, b: u8) -> u32 {
|
||||||
|
(b as u32) + ((g as u32) << 8) + ((r as u32) << 16) + ((a as u32) << 24)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the individual ARGB components out of a combined 32-bit color value which is in the
|
||||||
|
/// format 0xAARRGGBB
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `argb`: the 32-bit packed color
|
||||||
|
///
|
||||||
|
/// returns: the individual ARGB color components (0-255 each) in order: alpha, red, green, blue
|
||||||
|
#[inline]
|
||||||
|
pub fn from_argb32(argb: u32) -> (u8, u8, u8, u8) {
|
||||||
|
let a = ((argb & 0xff000000) >> 24) as u8;
|
||||||
|
let r = ((argb & 0x00ff0000) >> 16) as u8;
|
||||||
|
let g = ((argb & 0x0000ff00) >> 8) as u8;
|
||||||
|
let b = (argb & 0x000000ff) as u8;
|
||||||
|
(a, r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a set of individual RGB components to a combined 32-bit color value, packed into
|
||||||
|
/// the format 0xAARRGGBB. Substitutes a value of 255 for the missing alpha component.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `r`: the red component (0-255)
|
||||||
|
/// * `g`: the green component (0-255)
|
||||||
|
/// * `b`: the blue component (0-255)
|
||||||
|
///
|
||||||
|
/// returns: the u32 packed color
|
||||||
|
#[inline]
|
||||||
|
pub fn to_rgb32(r: u8, g: u8, b: u8) -> u32 {
|
||||||
|
to_argb32(255, r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extracts the individual RGB components out of a combined 32-bit color value which is in the
|
||||||
|
/// format 0xAARRGGBB. Ignores the alpha component.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `argb`: the 32-bit packed color
|
||||||
|
///
|
||||||
|
/// returns: the individual ARGB color components (0-255 each) in order: red, green, blue
|
||||||
|
#[inline]
|
||||||
|
pub fn from_rgb32(rgb: u32) -> (u8, u8, u8) {
|
||||||
|
// ignore alpha component at 0xff000000 ...
|
||||||
|
let r = ((rgb & 0x00ff0000) >> 16) as u8;
|
||||||
|
let g = ((rgb & 0x0000ff00) >> 8) as u8;
|
||||||
|
let b = (rgb & 0x000000ff) as u8;
|
||||||
|
(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linearly interpolates between two 32-bit packed colors in the format 0xAARRGGBB.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `a`: the first 32-bit packed color
|
||||||
|
/// * `b`: the second 32-bit packed color
|
||||||
|
/// * `t`: the amount to interpolate between the two values, specified as a fraction.
|
||||||
|
#[inline]
|
||||||
|
pub fn lerp_argb32(a: u32, b: u32, t: f32) -> u32 {
|
||||||
|
let (a1, r1, g1, b1) = from_argb32(a);
|
||||||
|
let (a2, r2, g2, b2) = from_argb32(b);
|
||||||
|
to_argb32(
|
||||||
|
((a1 as f32) + ((a2 as f32) - (a1 as f32)) * t) as u8,
|
||||||
|
((r1 as f32) + ((r2 as f32) - (r1 as f32)) * t) as u8,
|
||||||
|
((g1 as f32) + ((g2 as f32) - (g1 as f32)) * t) as u8,
|
||||||
|
((b1 as f32) + ((b2 as f32) - (b1 as f32)) * t) as u8,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linearly interpolates between two 32-bit packed colors in the format 0xAARRGGBB. Ignores the
|
||||||
|
/// alpha component, which will always be set to 255 in the return value.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `a`: the first 32-bit packed color
|
||||||
|
/// * `b`: the second 32-bit packed color
|
||||||
|
/// * `t`: the amount to interpolate between the two values, specified as a fraction.
|
||||||
|
#[inline]
|
||||||
|
pub fn lerp_rgb32(a: u32, b: u32, t: f32) -> u32 {
|
||||||
|
let (r1, g1, b1) = from_rgb32(a);
|
||||||
|
let (r2, g2, b2) = from_rgb32(b);
|
||||||
|
to_rgb32(
|
||||||
|
((r1 as f32) + ((r2 as f32) - (r1 as f32)) * t) as u8,
|
||||||
|
((g1 as f32) + ((g2 as f32) - (g1 as f32)) * t) as u8,
|
||||||
|
((b1 as f32) + ((b2 as f32) - (b1 as f32)) * t) as u8,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// vga bios (0-63) format
|
||||||
|
fn read_256color_6bit_palette<T: ReadBytesExt>(
|
||||||
|
reader: &mut T,
|
||||||
|
) -> Result<[u32; NUM_COLORS], PaletteError> {
|
||||||
|
let mut colors = [0u32; NUM_COLORS];
|
||||||
|
for color in colors.iter_mut() {
|
||||||
|
let r = reader.read_u8()?;
|
||||||
|
let g = reader.read_u8()?;
|
||||||
|
let b = reader.read_u8()?;
|
||||||
|
*color = to_rgb32(r * 4, g * 4, b * 4);
|
||||||
|
}
|
||||||
|
Ok(colors)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_256color_6bit_palette<T: WriteBytesExt>(
|
||||||
|
writer: &mut T,
|
||||||
|
colors: &[u32; NUM_COLORS],
|
||||||
|
) -> Result<(), PaletteError> {
|
||||||
|
for color in colors.iter() {
|
||||||
|
let (r, g, b) = from_rgb32(*color);
|
||||||
|
writer.write_u8(r / 4)?;
|
||||||
|
writer.write_u8(g / 4)?;
|
||||||
|
writer.write_u8(b / 4)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
// normal (0-255) format
|
||||||
|
fn read_256color_8bit_palette<T: ReadBytesExt>(
|
||||||
|
reader: &mut T,
|
||||||
|
) -> Result<[u32; NUM_COLORS], PaletteError> {
|
||||||
|
let mut colors = [0u32; NUM_COLORS];
|
||||||
|
for color in colors.iter_mut() {
|
||||||
|
let r = reader.read_u8()?;
|
||||||
|
let g = reader.read_u8()?;
|
||||||
|
let b = reader.read_u8()?;
|
||||||
|
*color = to_rgb32(r, g, b);
|
||||||
|
}
|
||||||
|
Ok(colors)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_256color_8bit_palette<T: WriteBytesExt>(
|
||||||
|
writer: &mut T,
|
||||||
|
colors: &[u32; NUM_COLORS],
|
||||||
|
) -> Result<(), PaletteError> {
|
||||||
|
for color in colors.iter() {
|
||||||
|
let (r, g, b) = from_rgb32(*color);
|
||||||
|
writer.write_u8(r)?;
|
||||||
|
writer.write_u8(g)?;
|
||||||
|
writer.write_u8(b)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum PaletteError {
|
||||||
|
#[error("Palette I/O error")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PaletteFormat {
|
||||||
|
/// Individual RGB components in 6-bits (0-63) for VGA BIOS compatibility
|
||||||
|
Vga,
|
||||||
|
/// Individual RGB components in 8-bits (0-255)
|
||||||
|
Normal,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains a 256 color palette, and provides methods useful for working with palettes. The
|
||||||
|
/// colors are all stored individually as 32-bit packed values in the format 0xAARRGGBB.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Palette {
|
||||||
|
colors: [u32; NUM_COLORS],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Palette {
|
||||||
|
/// Creates a new Palette with all black colors.
|
||||||
|
pub fn new() -> Palette {
|
||||||
|
Palette {
|
||||||
|
colors: [0; NUM_COLORS],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new Palette, pre-loaded with the default VGA BIOS colors.
|
||||||
|
pub fn new_vga_palette() -> Result<Palette, PaletteError> {
|
||||||
|
Palette::load_from_bytes(&mut Cursor::new(VGA_PALETTE_BYTES), PaletteFormat::Vga)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads and returns a Palette from a palette file on disk.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `path`: the path of the palette file to be loaded
|
||||||
|
/// * `format`: the format that the palette data is expected to be in
|
||||||
|
pub fn load_from_file(path: &Path, format: PaletteFormat) -> Result<Palette, PaletteError> {
|
||||||
|
let f = File::open(path)?;
|
||||||
|
let mut reader = BufReader::new(f);
|
||||||
|
Self::load_from_bytes(&mut reader, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads and returns a Palette from a reader. The data being loaded is expected to be the same
|
||||||
|
/// as if the palette was being loaded from a file on disk.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `reader`: the reader to load the palette from
|
||||||
|
/// * `format`: the format that the palette data is expected to be in
|
||||||
|
pub fn load_from_bytes<T: ReadBytesExt>(
|
||||||
|
reader: &mut T,
|
||||||
|
format: PaletteFormat,
|
||||||
|
) -> Result<Palette, PaletteError> {
|
||||||
|
let colors = match format {
|
||||||
|
PaletteFormat::Vga => read_256color_6bit_palette(reader)?,
|
||||||
|
PaletteFormat::Normal => read_256color_8bit_palette(reader)?,
|
||||||
|
};
|
||||||
|
Ok(Palette { colors })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the palette to a file on disk. If the file already exists, it will be overwritten.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `path`: the path of the file to save the palette to
|
||||||
|
/// * `format`: the format to write the palette data in
|
||||||
|
pub fn to_file(&self, path: &Path, format: PaletteFormat) -> Result<(), PaletteError> {
|
||||||
|
let f = File::create(path)?;
|
||||||
|
let mut writer = BufWriter::new(f);
|
||||||
|
self.to_bytes(&mut writer, format)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes the palette to a writer, in the same format as if it was writing to a file on disk.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `writer`: the writer to write palette data to
|
||||||
|
/// * `format`: the format to write the palette data in
|
||||||
|
pub fn to_bytes<T: WriteBytesExt>(
|
||||||
|
&self,
|
||||||
|
writer: &mut T,
|
||||||
|
format: PaletteFormat,
|
||||||
|
) -> Result<(), PaletteError> {
|
||||||
|
match format {
|
||||||
|
PaletteFormat::Vga => write_256color_6bit_palette(writer, &self.colors),
|
||||||
|
PaletteFormat::Normal => write_256color_8bit_palette(writer, &self.colors),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fades a single color in the palette from its current RGB values towards the given RGB
|
||||||
|
/// values by up to the step amount given. This function is intended to be run many times
|
||||||
|
/// over a number of frames where each run completes a small step towards the complete fade.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `color`: the color index to fade
|
||||||
|
/// * `target_r`: the target red component (0-255) to fade towards
|
||||||
|
/// * `target_g`: the target green component (0-255) to fade towards
|
||||||
|
/// * `target_b`: the target blue component (0-255) to fade towards
|
||||||
|
/// * `step`: the amount to "step" by towards the target RGB values
|
||||||
|
///
|
||||||
|
/// returns: true if the color has reached the target RGB values, false otherwise
|
||||||
|
pub fn fade_color_toward_rgb(
|
||||||
|
&mut self,
|
||||||
|
color: u8,
|
||||||
|
target_r: u8,
|
||||||
|
target_g: u8,
|
||||||
|
target_b: u8,
|
||||||
|
step: u8,
|
||||||
|
) -> bool {
|
||||||
|
let mut modified = false;
|
||||||
|
|
||||||
|
let (mut r, mut g, mut b) = from_rgb32(self.colors[color as usize]);
|
||||||
|
|
||||||
|
if r != target_r {
|
||||||
|
modified = true;
|
||||||
|
let diff_r = r.overflowing_sub(target_r).0;
|
||||||
|
if r > target_r {
|
||||||
|
r -= min(step, diff_r);
|
||||||
|
} else {
|
||||||
|
r += min(step, diff_r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if g != target_g {
|
||||||
|
modified = true;
|
||||||
|
let diff_g = g.overflowing_sub(target_g).0;
|
||||||
|
if g > target_g {
|
||||||
|
g -= min(step, diff_g);
|
||||||
|
} else {
|
||||||
|
g += min(step, diff_g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if b != target_b {
|
||||||
|
modified = true;
|
||||||
|
let diff_b = b.overflowing_sub(target_b).0;
|
||||||
|
if b > target_b {
|
||||||
|
b -= min(step, diff_b);
|
||||||
|
} else {
|
||||||
|
b += min(step, diff_b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if modified {
|
||||||
|
self.colors[color as usize] = to_rgb32(r, g, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
(target_r == r) && (target_g == g) && (target_b == b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fades a range of colors in the palette from their current RGB values all towards the given
|
||||||
|
/// RGB values by up to the step amount given. This function is intended to be run many times
|
||||||
|
/// over a number of frames where each run completes a small step towards the complete fade.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `colors`: the range of colors to be faded
|
||||||
|
/// * `target_r`: the target red component (0-255) to fade towards
|
||||||
|
/// * `target_g`: the target green component (0-255) to fade towards
|
||||||
|
/// * `target_b`: the target blue component (0-255) to fade towards
|
||||||
|
/// * `step`: the amount to "step" by towards the target RGB values
|
||||||
|
///
|
||||||
|
/// returns: true if all of the colors in the range have reached the target RGB values, false
|
||||||
|
/// otherwise
|
||||||
|
pub fn fade_colors_toward_rgb<T: ColorRange>(
|
||||||
|
&mut self,
|
||||||
|
colors: T,
|
||||||
|
target_r: u8,
|
||||||
|
target_g: u8,
|
||||||
|
target_b: u8,
|
||||||
|
step: u8,
|
||||||
|
) -> bool {
|
||||||
|
let mut all_faded = true;
|
||||||
|
for color in colors {
|
||||||
|
if !self.fade_color_toward_rgb(color, target_r, target_g, target_b, step) {
|
||||||
|
all_faded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all_faded
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fades a range of colors in the palette from their current RGB values all towards the RGB
|
||||||
|
/// values in the other palette specified, by up to the step amount given. This function is
|
||||||
|
/// intended to be run many times over a number of frames where each run completes a small step
|
||||||
|
/// towards the complete fade.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `colors`: the range of colors to be faded
|
||||||
|
/// * `palette`: the other palette to use as the target to fade towards
|
||||||
|
/// * `step`: the amount to "step" by towards the target RGB values
|
||||||
|
///
|
||||||
|
/// returns: true if all of the colors in the range have reached the RGB values from the other
|
||||||
|
/// target palette, false otherwise
|
||||||
|
pub fn fade_colors_toward_palette<T: ColorRange>(
|
||||||
|
&mut self,
|
||||||
|
colors: T,
|
||||||
|
palette: &Palette,
|
||||||
|
step: u8,
|
||||||
|
) -> bool {
|
||||||
|
let mut all_faded = true;
|
||||||
|
for color in colors {
|
||||||
|
let (r, g, b) = from_rgb32(palette[color]);
|
||||||
|
if !self.fade_color_toward_rgb(color, r, g, b, step) {
|
||||||
|
all_faded = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
all_faded
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linearly interpolates between the specified colors in two palettes, storing the
|
||||||
|
/// interpolation results in this palette.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `colors`: the range of colors to be interpolated
|
||||||
|
/// * `a`: the first palette
|
||||||
|
/// * `b`: the second palette
|
||||||
|
/// * `t`: the amount to interpolate between the two palettes, specified as a fraction
|
||||||
|
pub fn lerp<T: ColorRange>(&mut self, colors: T, a: &Palette, b: &Palette, t: f32) {
|
||||||
|
for color in colors {
|
||||||
|
self[color] = lerp_rgb32(a[color], b[color], t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rotates a range of colors in the palette by a given amount.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `colors`: the range of colors to be rotated
|
||||||
|
/// * `step`: the number of positions (and direction) to rotate all colors by
|
||||||
|
pub fn rotate_colors<T: ColorRange>(&mut self, colors: T, step: i8) {
|
||||||
|
use Bound::*;
|
||||||
|
let start = match colors.start_bound() {
|
||||||
|
Excluded(&start) => start + 1,
|
||||||
|
Included(&start) => start,
|
||||||
|
Unbounded => 0,
|
||||||
|
} as usize;
|
||||||
|
let end = match colors.end_bound() {
|
||||||
|
Excluded(&end) => end - 1,
|
||||||
|
Included(&end) => end,
|
||||||
|
Unbounded => 255,
|
||||||
|
} as usize;
|
||||||
|
let subset = &mut self.colors[start..=end];
|
||||||
|
match step.signum() {
|
||||||
|
-1 => subset.rotate_left(step.abs() as usize),
|
||||||
|
1 => subset.rotate_right(step.abs() as usize),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finds and returns the index of the closest color in this palette to the RGB values provided.
|
||||||
|
/// This will not always return great results. It depends largely on the palette and the RGB
|
||||||
|
/// values being searched (for example, searching for bright green 0,255,0 in a palette which
|
||||||
|
/// contains no green hues at all is not likely to return a useful result).
|
||||||
|
pub fn find_color(&self, r: u8, g: u8, b: u8) -> u8 {
|
||||||
|
let mut closest_distance = 255 * 3;
|
||||||
|
let mut closest = 0;
|
||||||
|
|
||||||
|
for (index, color) in self.colors.iter().enumerate() {
|
||||||
|
let (this_r, this_g, this_b) = from_rgb32(*color);
|
||||||
|
|
||||||
|
// this comparison method is using the sRGB Euclidean formula described here:
|
||||||
|
// https://en.wikipedia.org/wiki/Color_difference
|
||||||
|
|
||||||
|
let distance = abs_diff(this_r, r) as u32
|
||||||
|
+ abs_diff(this_g, g) as u32
|
||||||
|
+ abs_diff(this_b, b) as u32;
|
||||||
|
|
||||||
|
if distance < closest_distance {
|
||||||
|
closest = index as u8;
|
||||||
|
closest_distance = distance;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<u8> for Palette {
|
||||||
|
type Output = u32;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn index(&self, index: u8) -> &Self::Output {
|
||||||
|
&self.colors[index as usize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IndexMut<u8> for Palette {
|
||||||
|
#[inline]
|
||||||
|
fn index_mut(&mut self, index: u8) -> &mut Self::Output {
|
||||||
|
&mut self.colors[index as usize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for Palette {
|
||||||
|
#[inline]
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.colors == other.colors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn argb_conversions() {
|
||||||
|
let argb = to_argb32(0x11, 0x22, 0x33, 0x44);
|
||||||
|
assert_eq!(argb, 0x11223344);
|
||||||
|
|
||||||
|
let argb = to_rgb32(0x22, 0x33, 0x44);
|
||||||
|
assert_eq!(argb, 0xff223344);
|
||||||
|
|
||||||
|
let (a, r, g, b) = from_argb32(0x11223344);
|
||||||
|
assert_eq!(0x11, a);
|
||||||
|
assert_eq!(0x22, r);
|
||||||
|
assert_eq!(0x33, g);
|
||||||
|
assert_eq!(0x44, b);
|
||||||
|
|
||||||
|
let (r, g, b) = from_rgb32(0x11223344);
|
||||||
|
assert_eq!(0x22, r);
|
||||||
|
assert_eq!(0x33, g);
|
||||||
|
assert_eq!(0x44, b);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn get_and_set_colors() {
|
||||||
|
let mut palette = Palette::new();
|
||||||
|
assert_eq!(0, palette[0]);
|
||||||
|
assert_eq!(0, palette[1]);
|
||||||
|
palette[0] = 0x11223344;
|
||||||
|
assert_eq!(0x11223344, palette[0]);
|
||||||
|
assert_eq!(0, palette[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn assert_vga_palette(palette: &Palette) {
|
||||||
|
assert_eq!(0xff000000, palette[0]);
|
||||||
|
assert_eq!(0xff0000a8, palette[1]);
|
||||||
|
assert_eq!(0xff00a800, palette[2]);
|
||||||
|
assert_eq!(0xff00a8a8, palette[3]);
|
||||||
|
assert_eq!(0xffa80000, palette[4]);
|
||||||
|
assert_eq!(0xffa800a8, palette[5]);
|
||||||
|
assert_eq!(0xffa85400, palette[6]);
|
||||||
|
assert_eq!(0xffa8a8a8, palette[7]);
|
||||||
|
assert_eq!(0xff545454, palette[8]);
|
||||||
|
assert_eq!(0xff5454fc, palette[9]);
|
||||||
|
assert_eq!(0xff54fc54, palette[10]);
|
||||||
|
assert_eq!(0xff54fcfc, palette[11]);
|
||||||
|
assert_eq!(0xfffc5454, palette[12]);
|
||||||
|
assert_eq!(0xfffc54fc, palette[13]);
|
||||||
|
assert_eq!(0xfffcfc54, palette[14]);
|
||||||
|
assert_eq!(0xfffcfcfc, palette[15]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_and_save() -> Result<(), PaletteError> {
|
||||||
|
let tmp_dir = TempDir::new()?;
|
||||||
|
|
||||||
|
// vga format
|
||||||
|
|
||||||
|
let palette = Palette::load_from_file(Path::new("./assets/vga.pal"), PaletteFormat::Vga)?;
|
||||||
|
assert_vga_palette(&palette);
|
||||||
|
|
||||||
|
let save_path = tmp_dir.path().join("test_save_vga_format.pal");
|
||||||
|
palette.to_file(&save_path, PaletteFormat::Vga)?;
|
||||||
|
let reloaded_palette = Palette::load_from_file(&save_path, PaletteFormat::Vga)?;
|
||||||
|
assert_eq!(palette, reloaded_palette);
|
||||||
|
|
||||||
|
// normal format
|
||||||
|
|
||||||
|
let palette =
|
||||||
|
Palette::load_from_file(Path::new("./test-assets/dp2.pal"), PaletteFormat::Normal)?;
|
||||||
|
|
||||||
|
let save_path = tmp_dir.path().join("test_save_normal_format.pal");
|
||||||
|
palette.to_file(&save_path, PaletteFormat::Normal)?;
|
||||||
|
let reloaded_palette = Palette::load_from_file(&save_path, PaletteFormat::Normal)?;
|
||||||
|
assert_eq!(palette, reloaded_palette);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
68
libretrogd/src/lib.rs
Normal file
68
libretrogd/src/lib.rs
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
extern crate core;
|
||||||
|
extern crate sdl2;
|
||||||
|
|
||||||
|
pub use crate::graphics::bitmap::*;
|
||||||
|
pub use crate::graphics::bitmapatlas::*;
|
||||||
|
pub use crate::graphics::font::*;
|
||||||
|
pub use crate::graphics::palette::*;
|
||||||
|
pub use crate::graphics::*;
|
||||||
|
pub use crate::math::circle::*;
|
||||||
|
pub use crate::math::matrix3x3::*;
|
||||||
|
pub use crate::math::rect::*;
|
||||||
|
pub use crate::math::vector2::*;
|
||||||
|
pub use crate::math::*;
|
||||||
|
pub use crate::system::input_devices::keyboard::*;
|
||||||
|
pub use crate::system::input_devices::mouse::*;
|
||||||
|
pub use crate::system::input_devices::*;
|
||||||
|
pub use crate::system::*;
|
||||||
|
|
||||||
|
pub mod entities;
|
||||||
|
pub mod events;
|
||||||
|
pub mod graphics;
|
||||||
|
pub mod math;
|
||||||
|
pub mod states;
|
||||||
|
pub mod system;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
pub const LOW_RES: bool = if cfg!(feature = "low_res") {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
pub const WIDE_SCREEN: bool = if cfg!(feature = "wide") {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const SCREEN_WIDTH: u32 = if cfg!(feature = "low_res") {
|
||||||
|
if cfg!(feature = "wide") {
|
||||||
|
214
|
||||||
|
} else {
|
||||||
|
160
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if cfg!(feature = "wide") {
|
||||||
|
428
|
||||||
|
} else {
|
||||||
|
320
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pub const SCREEN_HEIGHT: u32 = if cfg!(feature = "low_res") {
|
||||||
|
120
|
||||||
|
} else {
|
||||||
|
240
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const SCREEN_TOP: u32 = 0;
|
||||||
|
pub const SCREEN_LEFT: u32 = 0;
|
||||||
|
pub const SCREEN_RIGHT: u32 = SCREEN_WIDTH - 1;
|
||||||
|
pub const SCREEN_BOTTOM: u32 = SCREEN_HEIGHT - 1;
|
||||||
|
|
||||||
|
pub const DEFAULT_SCALE_FACTOR: u32 = if cfg!(feature = "low_res") {
|
||||||
|
6
|
||||||
|
} else {
|
||||||
|
3
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const NUM_COLORS: usize = 256; // i mean ... the number of colors is really defined by the size of u8 ...
|
113
libretrogd/src/math/circle.rs
Normal file
113
libretrogd/src/math/circle.rs
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
use crate::{distance_between, distance_squared_between, Vector2};
|
||||||
|
|
||||||
|
/// Represents a 2D circle, using integer center coordinates and radius.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub struct Circle {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub radius: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Circle {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(x: i32, y: i32, radius: u32) -> Self {
|
||||||
|
Circle { x, y, radius }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_encapsulating(points: &[Vector2]) -> Option<Circle> {
|
||||||
|
if points.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut min_x = points[0].x;
|
||||||
|
let mut min_y = points[0].y;
|
||||||
|
let mut max_x = min_x;
|
||||||
|
let mut max_y = min_y;
|
||||||
|
|
||||||
|
for i in 0..points.len() {
|
||||||
|
let point = &points[i];
|
||||||
|
min_x = point.x.min(min_x);
|
||||||
|
min_y = point.y.min(min_y);
|
||||||
|
max_x = point.x.max(max_x);
|
||||||
|
max_y = point.y.max(max_y);
|
||||||
|
}
|
||||||
|
|
||||||
|
let radius = distance_between(min_x, min_y, max_x, max_y) * 0.5;
|
||||||
|
let center_x = (max_x - min_x) / 2.0;
|
||||||
|
let center_y = (max_y - min_y) / 2.0;
|
||||||
|
|
||||||
|
Some(Circle {
|
||||||
|
x: center_x as i32,
|
||||||
|
y: center_y as i32,
|
||||||
|
radius: radius as u32,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the diameter of the circle.
|
||||||
|
#[inline]
|
||||||
|
pub fn diameter(&self) -> u32 {
|
||||||
|
self.radius * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given point is contained within the bounds of this circle.
|
||||||
|
pub fn contains_point(&self, x: i32, y: i32) -> bool {
|
||||||
|
let distance_squared =
|
||||||
|
distance_squared_between(self.x as f32, self.y as f32, x as f32, y as f32);
|
||||||
|
let radius_squared = (self.radius * self.radius) as f32;
|
||||||
|
distance_squared <= radius_squared
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given circle at least partially overlaps the bounds of this circle.
|
||||||
|
pub fn overlaps(&self, other: &Circle) -> bool {
|
||||||
|
let distance_squared =
|
||||||
|
distance_squared_between(self.x as f32, self.y as f32, other.x as f32, other.y as f32);
|
||||||
|
let minimum_distance_squared =
|
||||||
|
((self.radius + other.radius) * (self.radius + other.radius)) as f32;
|
||||||
|
distance_squared <= minimum_distance_squared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_new() {
|
||||||
|
let c = Circle::new(1, 2, 4);
|
||||||
|
assert_eq!(1, c.x);
|
||||||
|
assert_eq!(2, c.y);
|
||||||
|
assert_eq!(4, c.radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_diameter() {
|
||||||
|
let c = Circle::new(4, 4, 3);
|
||||||
|
assert_eq!(6, c.diameter());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_contains_point() {
|
||||||
|
let c = Circle::new(1, 1, 6);
|
||||||
|
assert!(c.contains_point(4, 4));
|
||||||
|
assert!(!c.contains_point(8, 4));
|
||||||
|
|
||||||
|
let c = Circle::new(0, 1, 2);
|
||||||
|
assert!(!c.contains_point(3, 3));
|
||||||
|
assert!(c.contains_point(0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_overlaps() {
|
||||||
|
let a = Circle::new(3, 4, 5);
|
||||||
|
let b = Circle::new(14, 18, 8);
|
||||||
|
assert!(!a.overlaps(&b));
|
||||||
|
|
||||||
|
let a = Circle::new(2, 3, 12);
|
||||||
|
let b = Circle::new(15, 28, 10);
|
||||||
|
assert!(!a.overlaps(&b));
|
||||||
|
|
||||||
|
let a = Circle::new(-10, 8, 30);
|
||||||
|
let b = Circle::new(14, -24, 10);
|
||||||
|
assert!(a.overlaps(&b));
|
||||||
|
}
|
||||||
|
}
|
420
libretrogd/src/math/matrix3x3.rs
Normal file
420
libretrogd/src/math/matrix3x3.rs
Normal file
|
@ -0,0 +1,420 @@
|
||||||
|
use std::ops::{Mul, MulAssign};
|
||||||
|
|
||||||
|
use crate::{nearly_equal, Vector2};
|
||||||
|
|
||||||
|
/// Represents a 3x3 column-major matrix and provides common methods for matrix math.
|
||||||
|
#[derive(Debug, Copy, Clone)]
|
||||||
|
pub struct Matrix3x3 {
|
||||||
|
pub m: [f32; 9],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Matrix3x3 {
|
||||||
|
pub const M11: usize = 0;
|
||||||
|
pub const M12: usize = 3;
|
||||||
|
pub const M13: usize = 6;
|
||||||
|
pub const M21: usize = 1;
|
||||||
|
pub const M22: usize = 4;
|
||||||
|
pub const M23: usize = 7;
|
||||||
|
pub const M31: usize = 2;
|
||||||
|
pub const M32: usize = 5;
|
||||||
|
pub const M33: usize = 8;
|
||||||
|
|
||||||
|
pub const IDENTITY: Matrix3x3 = Matrix3x3 {
|
||||||
|
m: [1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0],
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Returns a new identity matrix.
|
||||||
|
#[inline]
|
||||||
|
pub fn identity() -> Matrix3x3 {
|
||||||
|
Matrix3x3::IDENTITY
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new matrix with the specified elements.
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
pub fn new(
|
||||||
|
m11: f32, m12: f32, m13: f32,
|
||||||
|
m21: f32, m22: f32, m23: f32,
|
||||||
|
m31: f32, m32: f32, m33: f32,
|
||||||
|
) -> Matrix3x3 {
|
||||||
|
Matrix3x3 {
|
||||||
|
m: [
|
||||||
|
m11, m21, m31,
|
||||||
|
m12, m22, m32,
|
||||||
|
m13, m23, m33
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new rotation matrix from a set of euler angles.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `x`: the x angle (in radians)
|
||||||
|
/// * `y`: the y angle (in radians)
|
||||||
|
/// * `z`: the z angle (in radians)
|
||||||
|
pub fn from_euler_angles(x: f32, y: f32, z: f32) -> Matrix3x3 {
|
||||||
|
let rotate_z = Matrix3x3::new_rotation_z(z);
|
||||||
|
let rotate_y = Matrix3x3::new_rotation_y(y);
|
||||||
|
let rotate_x = Matrix3x3::new_rotation_x(x);
|
||||||
|
|
||||||
|
// "right-to-left" column-major matrix concatenation
|
||||||
|
rotate_z * rotate_y * rotate_x
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new rotation matrix for rotation around the x axis.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `radians`: angle to rotate the x axis around (in radians)
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
pub fn new_rotation_x(radians: f32) -> Matrix3x3 {
|
||||||
|
let (s, c) = radians.sin_cos();
|
||||||
|
Matrix3x3::new(
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
0.0, c, -s,
|
||||||
|
0.0, s, c
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new rotation matrix for rotation around the y axis.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `radians`: angle to rotate the y axis around (in radians)
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
pub fn new_rotation_y(radians: f32) -> Matrix3x3 {
|
||||||
|
let (s, c) = radians.sin_cos();
|
||||||
|
Matrix3x3::new(
|
||||||
|
c, 0.0, s,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
-s, 0.0, c
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new rotation matrix for rotation around the z axis.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `radians`: angle to rotate the z axis around (in radians)
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
pub fn new_rotation_z(radians: f32) -> Matrix3x3 {
|
||||||
|
let (s, c) = radians.sin_cos();
|
||||||
|
Matrix3x3::new(
|
||||||
|
c, -s, 0.0,
|
||||||
|
s, c, 0.0,
|
||||||
|
0.0, 0.0, 1.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a translation matrix. For use with 2D coordinates only.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `x`: the amount to translate on the x axis
|
||||||
|
/// * `y`: the amount to translate on the y axis
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
pub fn new_2d_translation(x: f32, y: f32) -> Matrix3x3 {
|
||||||
|
Matrix3x3::new(
|
||||||
|
1.0, 0.0, 0.0,
|
||||||
|
0.0, 1.0, 0.0,
|
||||||
|
x, y, 1.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a scaling matrix from scaling factors for each axis. For use with 2D coordinates
|
||||||
|
/// only.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `x`: the scale factor for the x axis
|
||||||
|
/// * `y`: the scale factor for the y axis
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
pub fn new_2d_scaling(x: f32, y: f32) -> Matrix3x3 {
|
||||||
|
Matrix3x3::new(
|
||||||
|
x, 0.0, 0.0,
|
||||||
|
0.0, y, 0.0,
|
||||||
|
0.0, 0.0, 1.0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new rotation matrix. For use with 2D coordinates only.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `radians`: angle to rotate by (in radians)
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn new_2d_rotation(radians: f32) -> Matrix3x3 {
|
||||||
|
Matrix3x3::new_rotation_z(radians)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the determinant of this matrix.
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
pub fn determinant(&self) -> f32 {
|
||||||
|
self.m[Matrix3x3::M11] * self.m[Matrix3x3::M22] * self.m[Matrix3x3::M33] +
|
||||||
|
self.m[Matrix3x3::M12] * self.m[Matrix3x3::M23] * self.m[Matrix3x3::M31] +
|
||||||
|
self.m[Matrix3x3::M13] * self.m[Matrix3x3::M21] * self.m[Matrix3x3::M32] -
|
||||||
|
self.m[Matrix3x3::M11] * self.m[Matrix3x3::M23] * self.m[Matrix3x3::M32] -
|
||||||
|
self.m[Matrix3x3::M12] * self.m[Matrix3x3::M21] * self.m[Matrix3x3::M33] -
|
||||||
|
self.m[Matrix3x3::M13] * self.m[Matrix3x3::M22] * self.m[Matrix3x3::M31]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the inverse of this matrix.
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn invert(&self) -> Matrix3x3 {
|
||||||
|
let d = self.determinant();
|
||||||
|
if nearly_equal(d, 0.0, 0.000001) {
|
||||||
|
Matrix3x3::IDENTITY
|
||||||
|
} else {
|
||||||
|
let d = 1.0 / d;
|
||||||
|
Matrix3x3 {
|
||||||
|
m: [
|
||||||
|
d * (self.m[Matrix3x3::M22] * self.m[Matrix3x3::M33] - self.m[Matrix3x3::M32] * self.m[Matrix3x3::M23]),
|
||||||
|
d * (self.m[Matrix3x3::M31] * self.m[Matrix3x3::M23] - self.m[Matrix3x3::M21] * self.m[Matrix3x3::M33]),
|
||||||
|
d * (self.m[Matrix3x3::M21] * self.m[Matrix3x3::M32] - self.m[Matrix3x3::M31] * self.m[Matrix3x3::M22]),
|
||||||
|
d * (self.m[Matrix3x3::M32] * self.m[Matrix3x3::M13] - self.m[Matrix3x3::M12] * self.m[Matrix3x3::M33]),
|
||||||
|
d * (self.m[Matrix3x3::M11] * self.m[Matrix3x3::M33] - self.m[Matrix3x3::M31] * self.m[Matrix3x3::M13]),
|
||||||
|
d * (self.m[Matrix3x3::M31] * self.m[Matrix3x3::M12] - self.m[Matrix3x3::M11] * self.m[Matrix3x3::M32]),
|
||||||
|
d * (self.m[Matrix3x3::M12] * self.m[Matrix3x3::M23] - self.m[Matrix3x3::M22] * self.m[Matrix3x3::M13]),
|
||||||
|
d * (self.m[Matrix3x3::M21] * self.m[Matrix3x3::M13] - self.m[Matrix3x3::M11] * self.m[Matrix3x3::M23]),
|
||||||
|
d * (self.m[Matrix3x3::M11] * self.m[Matrix3x3::M22] - self.m[Matrix3x3::M21] * self.m[Matrix3x3::M12]),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the transpose of this matrix.
|
||||||
|
#[inline]
|
||||||
|
pub fn transpose(&self) -> Matrix3x3 {
|
||||||
|
Matrix3x3::new(
|
||||||
|
self.m[Matrix3x3::M11],
|
||||||
|
self.m[Matrix3x3::M21],
|
||||||
|
self.m[Matrix3x3::M31],
|
||||||
|
self.m[Matrix3x3::M12],
|
||||||
|
self.m[Matrix3x3::M22],
|
||||||
|
self.m[Matrix3x3::M32],
|
||||||
|
self.m[Matrix3x3::M13],
|
||||||
|
self.m[Matrix3x3::M23],
|
||||||
|
self.m[Matrix3x3::M33],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets all of the elements of this matrix.
|
||||||
|
#[inline]
|
||||||
|
pub fn set(
|
||||||
|
&mut self,
|
||||||
|
m11: f32,
|
||||||
|
m12: f32,
|
||||||
|
m13: f32,
|
||||||
|
m21: f32,
|
||||||
|
m22: f32,
|
||||||
|
m23: f32,
|
||||||
|
m31: f32,
|
||||||
|
m32: f32,
|
||||||
|
m33: f32,
|
||||||
|
) {
|
||||||
|
self.m[Matrix3x3::M11] = m11;
|
||||||
|
self.m[Matrix3x3::M12] = m12;
|
||||||
|
self.m[Matrix3x3::M13] = m13;
|
||||||
|
self.m[Matrix3x3::M21] = m21;
|
||||||
|
self.m[Matrix3x3::M22] = m22;
|
||||||
|
self.m[Matrix3x3::M23] = m23;
|
||||||
|
self.m[Matrix3x3::M31] = m31;
|
||||||
|
self.m[Matrix3x3::M32] = m32;
|
||||||
|
self.m[Matrix3x3::M33] = m33;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mul for Matrix3x3 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
fn mul(self, rhs: Self) -> Self::Output {
|
||||||
|
Matrix3x3::new(
|
||||||
|
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M31],
|
||||||
|
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M32],
|
||||||
|
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M33],
|
||||||
|
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M31],
|
||||||
|
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M32],
|
||||||
|
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M33],
|
||||||
|
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M31],
|
||||||
|
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M32],
|
||||||
|
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M33]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MulAssign for Matrix3x3 {
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
fn mul_assign(&mut self, rhs: Self) {
|
||||||
|
self.set(
|
||||||
|
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M31],
|
||||||
|
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M32],
|
||||||
|
self.m[Matrix3x3::M11] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M12] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M13] * rhs.m[Matrix3x3::M33],
|
||||||
|
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M31],
|
||||||
|
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M32],
|
||||||
|
self.m[Matrix3x3::M21] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M22] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M23] * rhs.m[Matrix3x3::M33],
|
||||||
|
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M11] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M21] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M31],
|
||||||
|
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M12] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M22] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M32],
|
||||||
|
self.m[Matrix3x3::M31] * rhs.m[Matrix3x3::M13] + self.m[Matrix3x3::M32] * rhs.m[Matrix3x3::M23] + self.m[Matrix3x3::M33] * rhs.m[Matrix3x3::M33]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mul<Vector2> for Matrix3x3 {
|
||||||
|
type Output = Vector2;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
fn mul(self, rhs: Vector2) -> Self::Output {
|
||||||
|
Vector2 {
|
||||||
|
x: rhs.x * self.m[Matrix3x3::M11] + rhs.y * self.m[Matrix3x3::M12] + self.m[Matrix3x3::M13] + self.m[Matrix3x3::M31],
|
||||||
|
y: rhs.x * self.m[Matrix3x3::M21] + rhs.y * self.m[Matrix3x3::M22] + self.m[Matrix3x3::M23] + self.m[Matrix3x3::M32]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use crate::math::{RADIANS_180, RADIANS_90};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_new() {
|
||||||
|
let m = Matrix3x3::new(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
|
||||||
|
assert_eq!(1.0, m.m[Matrix3x3::M11]);
|
||||||
|
assert_eq!(2.0, m.m[Matrix3x3::M12]);
|
||||||
|
assert_eq!(3.0, m.m[Matrix3x3::M13]);
|
||||||
|
assert_eq!(4.0, m.m[Matrix3x3::M21]);
|
||||||
|
assert_eq!(5.0, m.m[Matrix3x3::M22]);
|
||||||
|
assert_eq!(6.0, m.m[Matrix3x3::M23]);
|
||||||
|
assert_eq!(7.0, m.m[Matrix3x3::M31]);
|
||||||
|
assert_eq!(8.0, m.m[Matrix3x3::M32]);
|
||||||
|
assert_eq!(9.0, m.m[Matrix3x3::M33]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_set() {
|
||||||
|
let mut m = Matrix3x3 { m: [0.0; 9] };
|
||||||
|
m.set(1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0);
|
||||||
|
assert_eq!(1.0, m.m[Matrix3x3::M11]);
|
||||||
|
assert_eq!(2.0, m.m[Matrix3x3::M12]);
|
||||||
|
assert_eq!(3.0, m.m[Matrix3x3::M13]);
|
||||||
|
assert_eq!(4.0, m.m[Matrix3x3::M21]);
|
||||||
|
assert_eq!(5.0, m.m[Matrix3x3::M22]);
|
||||||
|
assert_eq!(6.0, m.m[Matrix3x3::M23]);
|
||||||
|
assert_eq!(7.0, m.m[Matrix3x3::M31]);
|
||||||
|
assert_eq!(8.0, m.m[Matrix3x3::M32]);
|
||||||
|
assert_eq!(9.0, m.m[Matrix3x3::M33]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_identity() {
|
||||||
|
let m = Matrix3x3::identity();
|
||||||
|
assert_eq!(1.0, m.m[Matrix3x3::M11]);
|
||||||
|
assert_eq!(0.0, m.m[Matrix3x3::M12]);
|
||||||
|
assert_eq!(0.0, m.m[Matrix3x3::M13]);
|
||||||
|
assert_eq!(0.0, m.m[Matrix3x3::M21]);
|
||||||
|
assert_eq!(1.0, m.m[Matrix3x3::M22]);
|
||||||
|
assert_eq!(0.0, m.m[Matrix3x3::M23]);
|
||||||
|
assert_eq!(0.0, m.m[Matrix3x3::M31]);
|
||||||
|
assert_eq!(0.0, m.m[Matrix3x3::M32]);
|
||||||
|
assert_eq!(1.0, m.m[Matrix3x3::M33]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[test]
|
||||||
|
pub fn test_transpose() {
|
||||||
|
let m = Matrix3x3::new(
|
||||||
|
1.0, 2.0, 3.0,
|
||||||
|
4.0, 5.0, 6.0,
|
||||||
|
7.0, 8.0, 9.0
|
||||||
|
);
|
||||||
|
let t = m.transpose();
|
||||||
|
assert_eq!(1.0, t.m[Matrix3x3::M11]);
|
||||||
|
assert_eq!(4.0, t.m[Matrix3x3::M12]);
|
||||||
|
assert_eq!(7.0, t.m[Matrix3x3::M13]);
|
||||||
|
assert_eq!(2.0, t.m[Matrix3x3::M21]);
|
||||||
|
assert_eq!(5.0, t.m[Matrix3x3::M22]);
|
||||||
|
assert_eq!(8.0, t.m[Matrix3x3::M23]);
|
||||||
|
assert_eq!(3.0, t.m[Matrix3x3::M31]);
|
||||||
|
assert_eq!(6.0, t.m[Matrix3x3::M32]);
|
||||||
|
assert_eq!(9.0, t.m[Matrix3x3::M33]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_mul() {
|
||||||
|
let a = Matrix3x3::new(12.0, 8.0, 4.0, 3.0, 17.0, 14.0, 9.0, 8.0, 10.0);
|
||||||
|
let b = Matrix3x3::new(5.0, 19.0, 3.0, 6.0, 15.0, 9.0, 7.0, 8.0, 16.0);
|
||||||
|
let c = a * b;
|
||||||
|
assert!(nearly_equal(136.0, c.m[Matrix3x3::M11], 0.001));
|
||||||
|
assert!(nearly_equal(380.0, c.m[Matrix3x3::M12], 0.001));
|
||||||
|
assert!(nearly_equal(172.0, c.m[Matrix3x3::M13], 0.001));
|
||||||
|
assert!(nearly_equal(215.0, c.m[Matrix3x3::M21], 0.001));
|
||||||
|
assert!(nearly_equal(424.0, c.m[Matrix3x3::M22], 0.001));
|
||||||
|
assert!(nearly_equal(386.0, c.m[Matrix3x3::M23], 0.001));
|
||||||
|
assert!(nearly_equal(163.0, c.m[Matrix3x3::M31], 0.001));
|
||||||
|
assert!(nearly_equal(371.0, c.m[Matrix3x3::M32], 0.001));
|
||||||
|
assert!(nearly_equal(259.0, c.m[Matrix3x3::M33], 0.001));
|
||||||
|
|
||||||
|
let mut a = Matrix3x3::new(12.0, 8.0, 4.0, 3.0, 17.0, 14.0, 9.0, 8.0, 10.0);
|
||||||
|
let b = Matrix3x3::new(5.0, 19.0, 3.0, 6.0, 15.0, 9.0, 7.0, 8.0, 16.0);
|
||||||
|
a *= b;
|
||||||
|
assert!(nearly_equal(136.0, a.m[Matrix3x3::M11], 0.001));
|
||||||
|
assert!(nearly_equal(380.0, a.m[Matrix3x3::M12], 0.001));
|
||||||
|
assert!(nearly_equal(172.0, a.m[Matrix3x3::M13], 0.001));
|
||||||
|
assert!(nearly_equal(215.0, a.m[Matrix3x3::M21], 0.001));
|
||||||
|
assert!(nearly_equal(424.0, a.m[Matrix3x3::M22], 0.001));
|
||||||
|
assert!(nearly_equal(386.0, a.m[Matrix3x3::M23], 0.001));
|
||||||
|
assert!(nearly_equal(163.0, a.m[Matrix3x3::M31], 0.001));
|
||||||
|
assert!(nearly_equal(371.0, a.m[Matrix3x3::M32], 0.001));
|
||||||
|
assert!(nearly_equal(259.0, a.m[Matrix3x3::M33], 0.001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_2d_translation() {
|
||||||
|
let v = Vector2::new(10.2, 5.7);
|
||||||
|
let m = Matrix3x3::new_2d_translation(2.0, 3.0);
|
||||||
|
let t = m * v;
|
||||||
|
assert!(nearly_equal(12.2, t.x, 0.001));
|
||||||
|
assert!(nearly_equal(8.7, t.y, 0.001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_2d_scaling() {
|
||||||
|
let v = Vector2::new(10.2, 5.7);
|
||||||
|
let m = Matrix3x3::new_2d_scaling(3.0, 4.0);
|
||||||
|
let t = m * v;
|
||||||
|
assert!(nearly_equal(30.6, t.x, 0.001));
|
||||||
|
assert!(nearly_equal(22.8, t.y, 0.001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_2d_rotation() {
|
||||||
|
let v = Vector2::new(0.0, 5.0);
|
||||||
|
let m = Matrix3x3::new_2d_rotation(RADIANS_90);
|
||||||
|
let t = m * v;
|
||||||
|
assert!(nearly_equal(-5.0, t.x, 0.001));
|
||||||
|
assert!(nearly_equal(0.0, t.y, 0.001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_2d_combined_transform() {
|
||||||
|
let a = Matrix3x3::new_2d_translation(-2.0, 0.0);
|
||||||
|
let b = Matrix3x3::new_2d_rotation(RADIANS_180);
|
||||||
|
let m = a * b;
|
||||||
|
let v = Vector2::new(0.0, 5.0);
|
||||||
|
let t = m * v;
|
||||||
|
assert!(nearly_equal(2.0, t.x, 0.001));
|
||||||
|
assert!(nearly_equal(-5.0, t.y, 0.001));
|
||||||
|
}
|
||||||
|
}
|
272
libretrogd/src/math/mod.rs
Normal file
272
libretrogd/src/math/mod.rs
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
use std::ops::{Add, Div, Mul, Sub};
|
||||||
|
|
||||||
|
pub mod circle;
|
||||||
|
pub mod matrix3x3;
|
||||||
|
pub mod rect;
|
||||||
|
pub mod vector2;
|
||||||
|
|
||||||
|
pub const PI: f32 = std::f32::consts::PI; // 180 degrees
|
||||||
|
pub const HALF_PI: f32 = PI / 2.0; // 90 degrees
|
||||||
|
pub const QUARTER_PI: f32 = PI / 4.0; // 45 degrees
|
||||||
|
pub const TWO_PI: f32 = PI * 2.0; // 360 degrees
|
||||||
|
|
||||||
|
pub const RADIANS_0: f32 = 0.0;
|
||||||
|
pub const RADIANS_45: f32 = PI / 4.0;
|
||||||
|
pub const RADIANS_90: f32 = PI / 2.0;
|
||||||
|
pub const RADIANS_135: f32 = (3.0 * PI) / 4.0;
|
||||||
|
pub const RADIANS_180: f32 = PI;
|
||||||
|
pub const RADIANS_225: f32 = (5.0 * PI) / 4.0;
|
||||||
|
pub const RADIANS_270: f32 = (3.0 * PI) / 2.0;
|
||||||
|
pub const RADIANS_315: f32 = (7.0 * PI) / 4.0;
|
||||||
|
pub const RADIANS_360: f32 = PI * 2.0;
|
||||||
|
|
||||||
|
pub const PI_OVER_180: f32 = PI / 180.0;
|
||||||
|
pub const INVERSE_PI_OVER_180: f32 = 180.0 / PI;
|
||||||
|
|
||||||
|
// 2d directions. intended to be usable in a 2d screen-space where the origin 0,0 is at the
|
||||||
|
// top-left of the screen, where moving up is accomplished by decrementing Y.
|
||||||
|
// TODO: this is not a perfect solution and does pose some problems in various math calculations ...
|
||||||
|
pub const UP: f32 = -RADIANS_90;
|
||||||
|
pub const DOWN: f32 = RADIANS_90;
|
||||||
|
pub const LEFT: f32 = RADIANS_180;
|
||||||
|
pub const RIGHT: f32 = RADIANS_0;
|
||||||
|
|
||||||
|
/// Returns true if the two f32 values are "close enough" to be considered equal using the
|
||||||
|
/// precision of the provided epsilon value.
|
||||||
|
#[inline]
|
||||||
|
pub fn nearly_equal(a: f32, b: f32, epsilon: f32) -> bool {
|
||||||
|
// HACK/TODO: this is a shitty way to do this. but it's "good enough" for me ...
|
||||||
|
(a - b).abs() <= epsilon
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Linearly interpolates between two values.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `a`: first value (low end of range)
|
||||||
|
/// * `b`: second value (high end of range)
|
||||||
|
/// * `t`: the amount to interpolate between the two values, specified as a fraction
|
||||||
|
#[inline]
|
||||||
|
pub fn lerp<N>(a: N, b: N, t: f32) -> N
|
||||||
|
where
|
||||||
|
N: Copy + Add<Output = N> + Sub<Output = N> + Mul<f32, Output = N>,
|
||||||
|
{
|
||||||
|
a + (b - a) * t
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Given a linearly interpolated value and the original range (high and low) of the linear
|
||||||
|
/// interpolation, this will return the original interpolation factor (as a fraction)
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `a`: first value (low end of range)
|
||||||
|
/// * `b`: second value (high end of range)
|
||||||
|
/// * `lerp_result`: the interpolated value between the range given
|
||||||
|
#[inline]
|
||||||
|
pub fn inverse_lerp<N>(a: N, b: N, lerp_result: N) -> f32
|
||||||
|
where
|
||||||
|
N: Copy + Sub<Output = N> + Div<N, Output = f32>,
|
||||||
|
{
|
||||||
|
(lerp_result - a) / (b - a)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Interpolates between two values using a cubic equation.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `a`: first value (low end of range)
|
||||||
|
/// * `b`: second value (high end of range)
|
||||||
|
/// * `t`: the amount to interpolate between the two values, specified as a fraction
|
||||||
|
#[inline]
|
||||||
|
pub fn smooth_lerp<N>(a: N, b: N, t: f32) -> N
|
||||||
|
where
|
||||||
|
N: Copy + Add<Output = N> + Sub<Output = N> + Mul<f32, Output = N>,
|
||||||
|
{
|
||||||
|
let t = t.clamp(0.0, 1.0);
|
||||||
|
lerp(a, b, (t * t) * (3.0 - (2.0 * t)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-scales a given value from an old min/max range to a new and different min/max range such
|
||||||
|
/// that the returned value is approximately at the same relative position within the new min/max
|
||||||
|
/// range.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `value`: the value to be re-scaled which is currently between `old_min` and `old_max`
|
||||||
|
/// * `old_min`: original min value (low end of range)
|
||||||
|
/// * `old_max`: original max value (high end of range)
|
||||||
|
/// * `new_min`: new min value (low end of range)
|
||||||
|
/// * `new_max`: new max value (high end of range)
|
||||||
|
#[inline]
|
||||||
|
pub fn scale_range<N>(value: N, old_min: N, old_max: N, new_min: N, new_max: N) -> N
|
||||||
|
where
|
||||||
|
N: Copy + Add<Output = N> + Sub<Output = N> + Mul<Output = N> + Div<Output = N>,
|
||||||
|
{
|
||||||
|
(new_max - new_min) * (value - old_min) / (old_max - old_min) + new_min
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the angle (in radians) between the two points.
|
||||||
|
#[inline]
|
||||||
|
pub fn angle_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
|
||||||
|
let delta_x = x2 - x1;
|
||||||
|
let delta_y = y2 - y1;
|
||||||
|
delta_y.atan2(delta_x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the X and Y point of a normalized 2D vector that points in the same direction as
|
||||||
|
/// the given angle.
|
||||||
|
#[inline]
|
||||||
|
pub fn angle_to_direction(radians: f32) -> (f32, f32) {
|
||||||
|
let x = radians.cos();
|
||||||
|
let y = radians.sin();
|
||||||
|
(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the squared distance between two 2D points.
|
||||||
|
#[inline]
|
||||||
|
pub fn distance_squared_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
|
||||||
|
(x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the distance between two 2D points.
|
||||||
|
#[inline]
|
||||||
|
pub fn distance_between(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
|
||||||
|
distance_squared_between(x1, y1, x2, y2).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait NearlyEqual {
|
||||||
|
type Output;
|
||||||
|
|
||||||
|
/// Returns true if the two f32 values are "close enough" to be considered equal using the
|
||||||
|
/// precision of the provided epsilon value.
|
||||||
|
fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NearlyEqual for f32 {
|
||||||
|
type Output = f32;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool {
|
||||||
|
nearly_equal(self, other, epsilon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait WrappingRadians {
|
||||||
|
type Type;
|
||||||
|
|
||||||
|
/// Adds two angle values in radians together returning the result. The addition will wrap
|
||||||
|
/// around so that the returned result always lies within 0 -> 2π radians (0 -> 360 degrees).
|
||||||
|
fn wrapping_radians_add(self, other: Self::Type) -> Self::Type;
|
||||||
|
|
||||||
|
/// Subtracts two angle values in radians returning the result. The subtraction will wrap
|
||||||
|
/// around so that the returned result always lies within 0 -> 2π radians (0 -> 360 degrees).
|
||||||
|
fn wrapping_radians_sub(self, other: Self::Type) -> Self::Type;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WrappingRadians for f32 {
|
||||||
|
type Type = f32;
|
||||||
|
|
||||||
|
fn wrapping_radians_add(self, other: Self::Type) -> Self::Type {
|
||||||
|
let result = self + other;
|
||||||
|
if result < RADIANS_0 {
|
||||||
|
result + RADIANS_360
|
||||||
|
} else if result >= RADIANS_360 {
|
||||||
|
result - RADIANS_360
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wrapping_radians_sub(self, other: Self::Type) -> Self::Type {
|
||||||
|
let result = self - other;
|
||||||
|
if result < RADIANS_0 {
|
||||||
|
result + RADIANS_360
|
||||||
|
} else if result >= RADIANS_360 {
|
||||||
|
result - RADIANS_360
|
||||||
|
} else {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_nearly_equal() {
|
||||||
|
assert!(nearly_equal(4.0, 4.0, 0.1));
|
||||||
|
assert!(4.0f32.nearly_equal(4.0, 0.1));
|
||||||
|
assert!(nearly_equal(0.1 + 0.2, 0.3, 0.01));
|
||||||
|
assert!(!nearly_equal(1.0001, 1.0005, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_lerp() {
|
||||||
|
assert!(nearly_equal(15.0, lerp(10.0, 20.0, 0.5), 0.0001));
|
||||||
|
assert!(nearly_equal(10.0, lerp(10.0, 20.0, 0.0), 0.0001));
|
||||||
|
assert!(nearly_equal(20.0, lerp(10.0, 20.0, 1.0), 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_inverse_lerp() {
|
||||||
|
assert_eq!(0.5, inverse_lerp(10.0, 20.0, 15.0f32))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_angle_between() {
|
||||||
|
let angle = angle_between(20.0, 20.0, 10.0, 10.0);
|
||||||
|
assert!(nearly_equal(-RADIANS_135, angle, 0.0001));
|
||||||
|
let angle = angle_between(0.0, 0.0, 10.0, 10.0);
|
||||||
|
assert!(nearly_equal(RADIANS_45, angle, 0.0001));
|
||||||
|
let angle = angle_between(5.0, 5.0, 5.0, 5.0);
|
||||||
|
assert!(nearly_equal(0.0, angle, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_angle_to_direction() {
|
||||||
|
let (x, y) = angle_to_direction(RADIANS_0);
|
||||||
|
assert!(nearly_equal(x, 1.0, 0.000001));
|
||||||
|
assert!(nearly_equal(y, 0.0, 0.000001));
|
||||||
|
let (x, y) = angle_to_direction(RADIANS_45);
|
||||||
|
assert!(nearly_equal(x, 0.707106, 0.000001));
|
||||||
|
assert!(nearly_equal(y, 0.707106, 0.000001));
|
||||||
|
let (x, y) = angle_to_direction(RADIANS_225);
|
||||||
|
assert!(nearly_equal(x, -0.707106, 0.000001));
|
||||||
|
assert!(nearly_equal(y, -0.707106, 0.000001));
|
||||||
|
|
||||||
|
let (x, y) = angle_to_direction(UP);
|
||||||
|
assert!(nearly_equal(x, 0.0, 0.000001));
|
||||||
|
assert!(nearly_equal(y, -1.0, 0.000001));
|
||||||
|
let (x, y) = angle_to_direction(DOWN);
|
||||||
|
assert!(nearly_equal(x, 0.0, 0.000001));
|
||||||
|
assert!(nearly_equal(y, 1.0, 0.000001));
|
||||||
|
let (x, y) = angle_to_direction(LEFT);
|
||||||
|
assert!(nearly_equal(x, -1.0, 0.000001));
|
||||||
|
assert!(nearly_equal(y, 0.0, 0.000001));
|
||||||
|
let (x, y) = angle_to_direction(RIGHT);
|
||||||
|
assert!(nearly_equal(x, 1.0, 0.000001));
|
||||||
|
assert!(nearly_equal(y, 0.0, 0.000001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_distance_between() {
|
||||||
|
let x1 = -2.0;
|
||||||
|
let y1 = 1.0;
|
||||||
|
let x2 = 4.0;
|
||||||
|
let y2 = 3.0;
|
||||||
|
let distance_squared = distance_squared_between(x1, y1, x2, y2);
|
||||||
|
let distance = distance_between(x1, y1, x2, y2);
|
||||||
|
assert!(nearly_equal(distance_squared, 40.0000, 0.0001));
|
||||||
|
assert!(nearly_equal(distance, 6.3245, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_wrapping_radians() {
|
||||||
|
assert!(nearly_equal(RADIANS_90, RADIANS_45.wrapping_radians_add(RADIANS_45), 0.0001));
|
||||||
|
assert!(nearly_equal(RADIANS_90, RADIANS_180.wrapping_radians_sub(RADIANS_90), 0.0001));
|
||||||
|
|
||||||
|
assert!(nearly_equal(RADIANS_45, RADIANS_315.wrapping_radians_add(RADIANS_90), 0.0001));
|
||||||
|
assert!(nearly_equal(RADIANS_315, RADIANS_90.wrapping_radians_sub(RADIANS_135), 0.0001));
|
||||||
|
}
|
||||||
|
}
|
284
libretrogd/src/math/rect.rs
Normal file
284
libretrogd/src/math/rect.rs
Normal file
|
@ -0,0 +1,284 @@
|
||||||
|
/// Represents a 2D rectangle, using integer coordinates and dimensions.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||||
|
pub struct Rect {
|
||||||
|
pub x: i32,
|
||||||
|
pub y: i32,
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rect {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(x: i32, y: i32, width: u32, height: u32) -> Rect {
|
||||||
|
Rect {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new rect from the specified coordinates. Automatically determines if the
|
||||||
|
/// coordinates provided are swapped (where the right/bottom coordinate is provided before the
|
||||||
|
/// left/top). All of the coordinates are inclusive.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `left`: the left x coordinate
|
||||||
|
/// * `top`: the top y coordinate
|
||||||
|
/// * `right`: the right x coordinate
|
||||||
|
/// * `bottom`: the bottom y coordinate
|
||||||
|
pub fn from_coords(left: i32, top: i32, right: i32, bottom: i32) -> Rect {
|
||||||
|
let x;
|
||||||
|
let y;
|
||||||
|
let width;
|
||||||
|
let height;
|
||||||
|
|
||||||
|
if left <= right {
|
||||||
|
x = left;
|
||||||
|
width = (right - left).abs() + 1;
|
||||||
|
} else {
|
||||||
|
x = right;
|
||||||
|
width = (left - right).abs() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if top <= bottom {
|
||||||
|
y = top;
|
||||||
|
height = (bottom - top).abs() + 1;
|
||||||
|
} else {
|
||||||
|
y = bottom;
|
||||||
|
height = (top - bottom).abs() + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rect {
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width: width as u32,
|
||||||
|
height: height as u32,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_from_coords(&mut self, left: i32, top: i32, right: i32, bottom: i32) {
|
||||||
|
if left <= right {
|
||||||
|
self.x = left;
|
||||||
|
self.width = ((right - left).abs() + 1) as u32;
|
||||||
|
} else {
|
||||||
|
self.x = right;
|
||||||
|
self.width = ((left - right).abs() + 1) as u32;
|
||||||
|
}
|
||||||
|
|
||||||
|
if top <= bottom {
|
||||||
|
self.y = top;
|
||||||
|
self.height = ((bottom - top).abs() + 1) as u32;
|
||||||
|
} else {
|
||||||
|
self.y = bottom;
|
||||||
|
self.height = ((top - bottom).abs() + 1) as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the right-most x coordinate contained by this rect.
|
||||||
|
#[inline]
|
||||||
|
pub fn right(&self) -> i32 {
|
||||||
|
if self.width > 0 {
|
||||||
|
self.x + self.width as i32 - 1
|
||||||
|
} else {
|
||||||
|
self.x
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the bottom-most y coordinate contained by this rect.
|
||||||
|
#[inline]
|
||||||
|
pub fn bottom(&self) -> i32 {
|
||||||
|
if self.height > 0 {
|
||||||
|
self.y + self.height as i32 - 1
|
||||||
|
} else {
|
||||||
|
self.y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given point is contained within the bounds of this rect.
|
||||||
|
pub fn contains_point(&self, x: i32, y: i32) -> bool {
|
||||||
|
(self.x <= x) && (self.right() >= x) && (self.y <= y) && (self.bottom() >= y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given rect is contained completely within the bounds of this rect.
|
||||||
|
pub fn contains_rect(&self, other: &Rect) -> bool {
|
||||||
|
(other.x >= self.x && other.x < self.right())
|
||||||
|
&& (other.right() > self.x && other.right() <= self.right())
|
||||||
|
&& (other.y >= self.y && other.y < self.bottom())
|
||||||
|
&& (other.bottom() > self.y && other.bottom() <= self.bottom())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given rect at least partially overlaps the bounds of this rect.
|
||||||
|
pub fn overlaps(&self, other: &Rect) -> bool {
|
||||||
|
(self.x <= other.right())
|
||||||
|
&& (self.right() >= other.x)
|
||||||
|
&& (self.y <= other.bottom())
|
||||||
|
&& (self.bottom() >= other.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn clamp_to(&mut self, other: &Rect) -> bool {
|
||||||
|
if !self.overlaps(other) {
|
||||||
|
// not possible to clamp this rect to the other rect as they do not overlap at all
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
// the rects at least partially overlap, so we will clamp this rect to the overlapping
|
||||||
|
// region of the other rect
|
||||||
|
let mut x1 = self.x;
|
||||||
|
let mut y1 = self.y;
|
||||||
|
let mut x2 = self.right();
|
||||||
|
let mut y2 = self.bottom();
|
||||||
|
if y1 < other.y {
|
||||||
|
y1 = other.y;
|
||||||
|
}
|
||||||
|
if y1 > other.bottom() {
|
||||||
|
y1 = other.bottom();
|
||||||
|
}
|
||||||
|
if y2 < other.y {
|
||||||
|
y2 = other.y;
|
||||||
|
}
|
||||||
|
if y2 > other.bottom() {
|
||||||
|
y2 = other.bottom();
|
||||||
|
}
|
||||||
|
if x1 < other.x {
|
||||||
|
x1 = other.x;
|
||||||
|
}
|
||||||
|
if x1 > other.right() {
|
||||||
|
x1 = other.right();
|
||||||
|
}
|
||||||
|
if x2 < other.x {
|
||||||
|
x2 = other.x;
|
||||||
|
}
|
||||||
|
if x2 > other.right() {
|
||||||
|
x2 = other.right();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.set_from_coords(x1, y1, x2, y2);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn right_and_left() {
|
||||||
|
let rect = Rect {
|
||||||
|
x: 5,
|
||||||
|
y: 6,
|
||||||
|
width: 16,
|
||||||
|
height: 12,
|
||||||
|
};
|
||||||
|
assert_eq!(20, rect.right());
|
||||||
|
assert_eq!(17, rect.bottom());
|
||||||
|
|
||||||
|
let rect = Rect {
|
||||||
|
x: -11,
|
||||||
|
y: -25,
|
||||||
|
width: 16,
|
||||||
|
height: 12,
|
||||||
|
};
|
||||||
|
assert_eq!(4, rect.right());
|
||||||
|
assert_eq!(-14, rect.bottom());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn create_from_coords() {
|
||||||
|
let rect = Rect::from_coords(10, 15, 20, 30);
|
||||||
|
assert_eq!(10, rect.x);
|
||||||
|
assert_eq!(15, rect.y);
|
||||||
|
assert_eq!(11, rect.width);
|
||||||
|
assert_eq!(16, rect.height);
|
||||||
|
assert_eq!(20, rect.right());
|
||||||
|
assert_eq!(30, rect.bottom());
|
||||||
|
|
||||||
|
let rect = Rect::from_coords(-5, -13, 6, -2);
|
||||||
|
assert_eq!(-5, rect.x);
|
||||||
|
assert_eq!(-13, rect.y);
|
||||||
|
assert_eq!(12, rect.width);
|
||||||
|
assert_eq!(12, rect.height);
|
||||||
|
assert_eq!(6, rect.right());
|
||||||
|
assert_eq!(-2, rect.bottom());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn create_from_coords_with_swapped_order() {
|
||||||
|
let rect = Rect::from_coords(20, 30, 10, 15);
|
||||||
|
assert_eq!(10, rect.x);
|
||||||
|
assert_eq!(15, rect.y);
|
||||||
|
assert_eq!(11, rect.width);
|
||||||
|
assert_eq!(16, rect.height);
|
||||||
|
assert_eq!(20, rect.right());
|
||||||
|
assert_eq!(30, rect.bottom());
|
||||||
|
|
||||||
|
let rect = Rect::from_coords(6, -2, -5, -13);
|
||||||
|
assert_eq!(-5, rect.x);
|
||||||
|
assert_eq!(-13, rect.y);
|
||||||
|
assert_eq!(12, rect.width);
|
||||||
|
assert_eq!(12, rect.height);
|
||||||
|
assert_eq!(6, rect.right());
|
||||||
|
assert_eq!(-2, rect.bottom());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_contains_point() {
|
||||||
|
let r = Rect::from_coords(10, 10, 20, 20);
|
||||||
|
|
||||||
|
assert!(r.contains_point(10, 10));
|
||||||
|
assert!(r.contains_point(15, 15));
|
||||||
|
assert!(r.contains_point(20, 20));
|
||||||
|
|
||||||
|
assert!(!r.contains_point(12, 30));
|
||||||
|
assert!(!r.contains_point(8, 12));
|
||||||
|
assert!(!r.contains_point(25, 16));
|
||||||
|
assert!(!r.contains_point(17, 4));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_contains_rect() {
|
||||||
|
let r = Rect::from_coords(10, 10, 20, 20);
|
||||||
|
|
||||||
|
assert!(r.contains_rect(&Rect::from_coords(12, 12, 15, 15)));
|
||||||
|
assert!(r.contains_rect(&Rect::from_coords(10, 10, 15, 15)));
|
||||||
|
assert!(r.contains_rect(&Rect::from_coords(15, 15, 20, 20)));
|
||||||
|
assert!(r.contains_rect(&Rect::from_coords(10, 12, 20, 15)));
|
||||||
|
assert!(r.contains_rect(&Rect::from_coords(12, 10, 15, 20)));
|
||||||
|
|
||||||
|
assert!(!r.contains_rect(&Rect::from_coords(5, 5, 15, 15)));
|
||||||
|
assert!(!r.contains_rect(&Rect::from_coords(15, 15, 25, 25)));
|
||||||
|
|
||||||
|
assert!(!r.contains_rect(&Rect::from_coords(2, 2, 8, 4)));
|
||||||
|
assert!(!r.contains_rect(&Rect::from_coords(12, 21, 18, 25)));
|
||||||
|
assert!(!r.contains_rect(&Rect::from_coords(22, 12, 32, 17)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_overlaps() {
|
||||||
|
let r = Rect::from_coords(10, 10, 20, 20);
|
||||||
|
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(12, 12, 15, 15)));
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(10, 10, 15, 15)));
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(15, 15, 20, 20)));
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(10, 12, 20, 15)));
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(12, 10, 15, 20)));
|
||||||
|
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(12, 5, 18, 10)));
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(13, 20, 16, 25)));
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(5, 12, 10, 18)));
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(20, 13, 25, 16)));
|
||||||
|
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(5, 5, 15, 15)));
|
||||||
|
assert!(r.overlaps(&Rect::from_coords(15, 15, 25, 25)));
|
||||||
|
|
||||||
|
assert!(!r.overlaps(&Rect::from_coords(2, 2, 8, 4)));
|
||||||
|
assert!(!r.overlaps(&Rect::from_coords(12, 21, 18, 25)));
|
||||||
|
assert!(!r.overlaps(&Rect::from_coords(22, 12, 32, 17)));
|
||||||
|
|
||||||
|
assert!(!r.overlaps(&Rect::from_coords(12, 5, 18, 9)));
|
||||||
|
assert!(!r.overlaps(&Rect::from_coords(13, 21, 16, 25)));
|
||||||
|
assert!(!r.overlaps(&Rect::from_coords(5, 12, 9, 18)));
|
||||||
|
assert!(!r.overlaps(&Rect::from_coords(21, 13, 25, 16)));
|
||||||
|
}
|
||||||
|
}
|
479
libretrogd/src/math/vector2.rs
Normal file
479
libretrogd/src/math/vector2.rs
Normal file
|
@ -0,0 +1,479 @@
|
||||||
|
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};
|
||||||
|
|
||||||
|
use crate::Matrix3x3;
|
||||||
|
use crate::{angle_between, angle_to_direction, nearly_equal, NearlyEqual};
|
||||||
|
|
||||||
|
/// Represents a 2D vector and provides common methods for vector math.
|
||||||
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
|
pub struct Vector2 {
|
||||||
|
pub x: f32,
|
||||||
|
pub y: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Vector2 {
|
||||||
|
pub const ZERO: Vector2 = Vector2 { x: 0.0, y: 0.0 };
|
||||||
|
|
||||||
|
pub const UP: Vector2 = Vector2 { x: 0.0, y: -1.0 };
|
||||||
|
pub const DOWN: Vector2 = Vector2 { x: 0.0, y: 1.0 };
|
||||||
|
pub const LEFT: Vector2 = Vector2 { x: -1.0, y: 0.0 };
|
||||||
|
pub const RIGHT: Vector2 = Vector2 { x: 1.0, y: 0.0 };
|
||||||
|
|
||||||
|
pub const X_AXIS: Vector2 = Vector2 { x: 1.0, y: 0.0 };
|
||||||
|
pub const Y_AXIS: Vector2 = Vector2 { x: 0.0, y: 1.0 };
|
||||||
|
|
||||||
|
/// Creates a vector with the specified X and Y components.
|
||||||
|
#[inline]
|
||||||
|
pub fn new(x: f32, y: f32) -> Vector2 {
|
||||||
|
Vector2 { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a normalized vector that points in the same direction as the given angle.
|
||||||
|
#[inline]
|
||||||
|
pub fn from_angle(radians: f32) -> Vector2 {
|
||||||
|
let (x, y) = angle_to_direction(radians);
|
||||||
|
Vector2 { x, y }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the distance between this and another vector.
|
||||||
|
#[inline]
|
||||||
|
pub fn distance(&self, other: &Vector2) -> f32 {
|
||||||
|
self.distance_squared(other).sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the squared distance between this and another vector.
|
||||||
|
#[inline]
|
||||||
|
pub fn distance_squared(&self, other: &Vector2) -> f32 {
|
||||||
|
(other.x - self.x) * (other.x - self.x) + (other.y - self.y) * (other.y - self.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the dot product of this and another vector.
|
||||||
|
#[inline]
|
||||||
|
pub fn dot(&self, other: &Vector2) -> f32 {
|
||||||
|
(self.x * other.x) + (self.y * other.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the length (a.k.a. magnitude) of this vector.
|
||||||
|
#[inline]
|
||||||
|
pub fn length(&self) -> f32 {
|
||||||
|
self.length_squared().sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the squared length of this vector.
|
||||||
|
#[inline]
|
||||||
|
pub fn length_squared(&self) -> f32 {
|
||||||
|
(self.x * self.x) + (self.y * self.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a normalized vector from this vector.
|
||||||
|
pub fn normalize(&self) -> Vector2 {
|
||||||
|
let inverse_length = 1.0 / self.length();
|
||||||
|
Vector2 {
|
||||||
|
x: self.x * inverse_length,
|
||||||
|
y: self.y * inverse_length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an extended (or shrunk) vector from this vector, where the returned vector will
|
||||||
|
/// have a length exactly matching the specified length, but will retain the same direction.
|
||||||
|
pub fn extend(&self, length: f32) -> Vector2 {
|
||||||
|
*self * (length / self.length())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the angle (in radians) equivalent to the direction of this vector.
|
||||||
|
#[inline]
|
||||||
|
pub fn angle(&self) -> f32 {
|
||||||
|
self.y.atan2(self.x)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the angle (in radians) between the this and another vector.
|
||||||
|
#[inline]
|
||||||
|
pub fn angle_between(&self, other: &Vector2) -> f32 {
|
||||||
|
angle_between(self.x, self.y, other.x, other.y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Neg for Vector2 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn neg(self) -> Self::Output {
|
||||||
|
Vector2 {
|
||||||
|
x: -self.x,
|
||||||
|
y: -self.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Add for Vector2 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn add(self, rhs: Self) -> Self::Output {
|
||||||
|
Vector2 {
|
||||||
|
x: self.x + rhs.x,
|
||||||
|
y: self.y + rhs.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddAssign for Vector2 {
|
||||||
|
#[inline]
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
self.x += rhs.x;
|
||||||
|
self.y += rhs.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sub for Vector2 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn sub(self, rhs: Self) -> Self::Output {
|
||||||
|
Vector2 {
|
||||||
|
x: self.x - rhs.x,
|
||||||
|
y: self.y - rhs.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubAssign for Vector2 {
|
||||||
|
#[inline]
|
||||||
|
fn sub_assign(&mut self, rhs: Self) {
|
||||||
|
self.x -= rhs.x;
|
||||||
|
self.y -= rhs.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mul for Vector2 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn mul(self, rhs: Self) -> Self::Output {
|
||||||
|
Vector2 {
|
||||||
|
x: self.x * rhs.x,
|
||||||
|
y: self.y * rhs.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MulAssign for Vector2 {
|
||||||
|
#[inline]
|
||||||
|
fn mul_assign(&mut self, rhs: Self) {
|
||||||
|
self.x *= rhs.x;
|
||||||
|
self.y *= rhs.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Div for Vector2 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn div(self, rhs: Self) -> Self::Output {
|
||||||
|
Vector2 {
|
||||||
|
x: self.x / rhs.x,
|
||||||
|
y: self.y / rhs.y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DivAssign for Vector2 {
|
||||||
|
#[inline]
|
||||||
|
fn div_assign(&mut self, rhs: Self) {
|
||||||
|
self.x /= rhs.x;
|
||||||
|
self.y /= rhs.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mul<f32> for Vector2 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn mul(self, rhs: f32) -> Self::Output {
|
||||||
|
Vector2 {
|
||||||
|
x: self.x * rhs,
|
||||||
|
y: self.y * rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MulAssign<f32> for Vector2 {
|
||||||
|
#[inline]
|
||||||
|
fn mul_assign(&mut self, rhs: f32) {
|
||||||
|
self.x *= rhs;
|
||||||
|
self.y *= rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Div<f32> for Vector2 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn div(self, rhs: f32) -> Self::Output {
|
||||||
|
Vector2 {
|
||||||
|
x: self.x / rhs,
|
||||||
|
y: self.y / rhs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DivAssign<f32> for Vector2 {
|
||||||
|
#[inline]
|
||||||
|
fn div_assign(&mut self, rhs: f32) {
|
||||||
|
self.x /= rhs;
|
||||||
|
self.y /= rhs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NearlyEqual for Vector2 {
|
||||||
|
type Output = Self;
|
||||||
|
|
||||||
|
#[inline(always)]
|
||||||
|
fn nearly_equal(self, other: Self::Output, epsilon: f32) -> bool {
|
||||||
|
nearly_equal(self.x, other.x, epsilon) && nearly_equal(self.y, other.y, epsilon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MulAssign<Matrix3x3> for Vector2 {
|
||||||
|
#[rustfmt::skip]
|
||||||
|
#[inline]
|
||||||
|
fn mul_assign(&mut self, rhs: Matrix3x3) {
|
||||||
|
let x = self.x * rhs.m[Matrix3x3::M11] + self.y * rhs.m[Matrix3x3::M12] + rhs.m[Matrix3x3::M13] + rhs.m[Matrix3x3::M31];
|
||||||
|
let y = self.x * rhs.m[Matrix3x3::M21] + self.y * rhs.m[Matrix3x3::M22] + rhs.m[Matrix3x3::M23] + rhs.m[Matrix3x3::M32];
|
||||||
|
self.x = x;
|
||||||
|
self.y = y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use crate::math::*;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_new() {
|
||||||
|
let v = Vector2::new(3.0, 7.0);
|
||||||
|
assert!(nearly_equal(v.x, 3.0, 0.0001));
|
||||||
|
assert!(nearly_equal(v.y, 7.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_neg() {
|
||||||
|
let v = Vector2 { x: 1.0, y: 2.0 };
|
||||||
|
let neg = -v;
|
||||||
|
assert!(nearly_equal(neg.x, -1.0, 0.0001));
|
||||||
|
assert!(nearly_equal(neg.y, -2.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_add() {
|
||||||
|
let a = Vector2 { x: 3.0, y: 4.0 };
|
||||||
|
let b = Vector2 { x: 1.0, y: 2.0 };
|
||||||
|
let c = a + b;
|
||||||
|
assert!(nearly_equal(c.x, 4.0, 0.0001));
|
||||||
|
assert!(nearly_equal(c.y, 6.0, 0.0001));
|
||||||
|
|
||||||
|
let mut a = Vector2 { x: 3.0, y: 4.0 };
|
||||||
|
let b = Vector2 { x: 1.0, y: 2.0 };
|
||||||
|
a += b;
|
||||||
|
assert!(nearly_equal(a.x, 4.0, 0.0001));
|
||||||
|
assert!(nearly_equal(a.y, 6.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_sub() {
|
||||||
|
let a = Vector2 { x: 3.0, y: 4.0 };
|
||||||
|
let b = Vector2 { x: 1.0, y: 2.0 };
|
||||||
|
let c = a - b;
|
||||||
|
assert!(nearly_equal(c.x, 2.0, 0.0001));
|
||||||
|
assert!(nearly_equal(c.y, 2.0, 0.0001));
|
||||||
|
|
||||||
|
let mut a = Vector2 { x: 3.0, y: 4.0 };
|
||||||
|
let b = Vector2 { x: 1.0, y: 2.0 };
|
||||||
|
a -= b;
|
||||||
|
assert!(nearly_equal(a.x, 2.0, 0.0001));
|
||||||
|
assert!(nearly_equal(a.y, 2.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_mul() {
|
||||||
|
let a = Vector2 { x: 2.5, y: 6.0 };
|
||||||
|
let b = Vector2 { x: 1.25, y: 2.0 };
|
||||||
|
let c = a * b;
|
||||||
|
assert!(nearly_equal(c.x, 3.125, 0.0001));
|
||||||
|
assert!(nearly_equal(c.y, 12.0, 0.0001));
|
||||||
|
|
||||||
|
let mut a = Vector2 { x: 2.5, y: 6.0 };
|
||||||
|
let b = Vector2 { x: 1.25, y: 2.0 };
|
||||||
|
a *= b;
|
||||||
|
assert!(nearly_equal(a.x, 3.125, 0.0001));
|
||||||
|
assert!(nearly_equal(a.y, 12.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_div() {
|
||||||
|
let a = Vector2 { x: 2.5, y: 6.0 };
|
||||||
|
let b = Vector2 { x: 1.25, y: 2.0 };
|
||||||
|
let c = a / b;
|
||||||
|
assert!(nearly_equal(c.x, 2.0, 0.0001));
|
||||||
|
assert!(nearly_equal(c.y, 3.0, 0.0001));
|
||||||
|
|
||||||
|
let mut a = Vector2 { x: 2.5, y: 6.0 };
|
||||||
|
let b = Vector2 { x: 1.25, y: 2.0 };
|
||||||
|
a /= b;
|
||||||
|
assert!(nearly_equal(a.x, 2.0, 0.0001));
|
||||||
|
assert!(nearly_equal(a.y, 3.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_scalar_mul() {
|
||||||
|
let a = Vector2 { x: 1.0, y: 2.0 };
|
||||||
|
let b = a * 2.0;
|
||||||
|
assert!(nearly_equal(b.x, 2.0, 0.0001));
|
||||||
|
assert!(nearly_equal(b.y, 4.0, 0.0001));
|
||||||
|
|
||||||
|
let mut a = Vector2 { x: 1.0, y: 2.0 };
|
||||||
|
a *= 2.0;
|
||||||
|
assert!(nearly_equal(b.x, 2.0, 0.0001));
|
||||||
|
assert!(nearly_equal(b.y, 4.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_scalar_div() {
|
||||||
|
let a = Vector2 { x: 1.0, y: 2.0 };
|
||||||
|
let b = a / 2.0;
|
||||||
|
assert!(nearly_equal(b.x, 0.5, 0.0001));
|
||||||
|
assert!(nearly_equal(b.y, 1.0, 0.0001));
|
||||||
|
|
||||||
|
let mut a = Vector2 { x: 1.0, y: 2.0 };
|
||||||
|
a /= 2.0;
|
||||||
|
assert!(nearly_equal(b.x, 0.5, 0.0001));
|
||||||
|
assert!(nearly_equal(b.y, 1.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_nearly_equal() {
|
||||||
|
let a = Vector2 { x: 3.4, y: -7.1 };
|
||||||
|
let b = Vector2 { x: 3.5, y: -7.1 };
|
||||||
|
assert!(!a.nearly_equal(b, 0.0001));
|
||||||
|
|
||||||
|
let a = Vector2 { x: 2.0, y: 4.0 };
|
||||||
|
let b = Vector2 { x: 2.0, y: 4.0 };
|
||||||
|
assert!(a.nearly_equal(b, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_length() {
|
||||||
|
let v = Vector2 { x: 6.0, y: 8.0 };
|
||||||
|
let length_squared = v.length_squared();
|
||||||
|
let length = v.length();
|
||||||
|
assert!(nearly_equal(length_squared, 100.0, 0.0001));
|
||||||
|
assert!(nearly_equal(length, 10.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_dot() {
|
||||||
|
let a = Vector2 { x: -6.0, y: 8.0 };
|
||||||
|
let b = Vector2 { x: 5.0, y: 12.0 };
|
||||||
|
let dot = a.dot(&b);
|
||||||
|
assert!(nearly_equal(dot, 66.0, 0.0001));
|
||||||
|
|
||||||
|
let a = Vector2 { x: -12.0, y: 16.0 };
|
||||||
|
let b = Vector2 { x: 12.0, y: 9.0 };
|
||||||
|
let dot = a.dot(&b);
|
||||||
|
assert!(nearly_equal(dot, 0.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_distance() {
|
||||||
|
let a = Vector2 { x: 1.0, y: 1.0 };
|
||||||
|
let b = Vector2 { x: 1.0, y: 3.0 };
|
||||||
|
let distance_squared = a.distance_squared(&b);
|
||||||
|
let distance = a.distance(&b);
|
||||||
|
assert!(nearly_equal(distance_squared, 4.0, 0.0001));
|
||||||
|
assert!(nearly_equal(distance, 2.0, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_normalize() {
|
||||||
|
let v = Vector2 { x: 3.0, y: 4.0 };
|
||||||
|
let normalized = v.normalize();
|
||||||
|
assert!(nearly_equal(normalized.x, 0.6, 0.0001));
|
||||||
|
assert!(nearly_equal(normalized.y, 0.8, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_extend() {
|
||||||
|
let v = Vector2 { x: 10.0, y: 1.0 };
|
||||||
|
let extended = v.extend(2.0);
|
||||||
|
assert!(nearly_equal(extended.x, 1.990, 0.0001));
|
||||||
|
assert!(nearly_equal(extended.y, 0.199, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub fn test_angle() {
|
||||||
|
assert!(nearly_equal(RADIANS_0, Vector2::new(5.0, 0.0).angle(), 0.0001));
|
||||||
|
assert!(nearly_equal(RADIANS_45, Vector2::new(5.0, 5.0).angle(), 0.0001));
|
||||||
|
assert!(nearly_equal(RADIANS_90, Vector2::new(0.0, 5.0).angle(), 0.0001));
|
||||||
|
assert!(nearly_equal(RADIANS_135, Vector2::new(-5.0, 5.0).angle(), 0.0001));
|
||||||
|
assert!(nearly_equal(RADIANS_180, Vector2::new(-5.0, 0.0).angle(), 0.0001));
|
||||||
|
assert!(nearly_equal(-RADIANS_135, Vector2::new(-5.0, -5.0).angle(), 0.0001));
|
||||||
|
assert!(nearly_equal(-RADIANS_90, Vector2::new(0.0, -5.0).angle(), 0.0001));
|
||||||
|
assert!(nearly_equal(-RADIANS_45, Vector2::new(5.0, -5.0).angle(), 0.0001));
|
||||||
|
|
||||||
|
assert!(nearly_equal(crate::math::UP, Vector2::UP.angle(), 0.0001));
|
||||||
|
assert!(nearly_equal(crate::math::DOWN, Vector2::DOWN.angle(), 0.0001));
|
||||||
|
assert!(nearly_equal(crate::math::LEFT, Vector2::LEFT.angle(), 0.0001));
|
||||||
|
assert!(nearly_equal(crate::math::RIGHT, Vector2::RIGHT.angle(), 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_from_angle() {
|
||||||
|
let v = Vector2::from_angle(RADIANS_0);
|
||||||
|
assert!(nearly_equal(v.x, 1.0, 0.000001));
|
||||||
|
assert!(nearly_equal(v.y, 0.0, 0.000001));
|
||||||
|
let v = Vector2::from_angle(RADIANS_45);
|
||||||
|
assert!(nearly_equal(v.x, 0.707106, 0.000001));
|
||||||
|
assert!(nearly_equal(v.y, 0.707106, 0.000001));
|
||||||
|
let v = Vector2::from_angle(RADIANS_225);
|
||||||
|
assert!(nearly_equal(v.x, -0.707106, 0.000001));
|
||||||
|
assert!(nearly_equal(v.y, -0.707106, 0.000001));
|
||||||
|
|
||||||
|
let v = Vector2::from_angle(UP);
|
||||||
|
assert!(v.nearly_equal(Vector2::UP, 0.000001));
|
||||||
|
let v = Vector2::from_angle(DOWN);
|
||||||
|
assert!(v.nearly_equal(Vector2::DOWN, 0.000001));
|
||||||
|
let v = Vector2::from_angle(LEFT);
|
||||||
|
assert!(v.nearly_equal(Vector2::LEFT, 0.000001));
|
||||||
|
let v = Vector2::from_angle(RIGHT);
|
||||||
|
assert!(v.nearly_equal(Vector2::RIGHT, 0.000001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_angle_between() {
|
||||||
|
let a = Vector2::new(20.0, 20.0);
|
||||||
|
let b = Vector2::new(10.0, 10.0);
|
||||||
|
let angle = a.angle_between(&b);
|
||||||
|
assert!(nearly_equal(-RADIANS_135, angle, 0.0001));
|
||||||
|
|
||||||
|
let a = Vector2::new(0.0, 0.0);
|
||||||
|
let b = Vector2::new(10.0, 10.0);
|
||||||
|
let angle = a.angle_between(&b);
|
||||||
|
assert!(nearly_equal(RADIANS_45, angle, 0.0001));
|
||||||
|
|
||||||
|
let a = Vector2::new(5.0, 5.0);
|
||||||
|
let b = Vector2::new(5.0, 5.0);
|
||||||
|
let angle = a.angle_between(&b);
|
||||||
|
assert!(nearly_equal(0.0, angle, 0.0001));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
pub fn test_lerp() {
|
||||||
|
let a = Vector2 { x: 5.0, y: 1.0 };
|
||||||
|
let b = Vector2 { x: 10.0, y: 2.0 };
|
||||||
|
let c = lerp(a, b, 0.5);
|
||||||
|
assert!(nearly_equal(c.x, 7.5, 0.0001));
|
||||||
|
assert!(nearly_equal(c.y, 1.5, 0.0001));
|
||||||
|
}
|
||||||
|
}
|
1264
libretrogd/src/states/mod.rs
Normal file
1264
libretrogd/src/states/mod.rs
Normal file
File diff suppressed because it is too large
Load diff
93
libretrogd/src/system/input_devices/keyboard.rs
Normal file
93
libretrogd/src/system/input_devices/keyboard.rs
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
use sdl2::event::Event;
|
||||||
|
use sdl2::keyboard::Scancode;
|
||||||
|
|
||||||
|
use super::{ButtonState, InputDevice};
|
||||||
|
|
||||||
|
const MAX_KEYS: usize = 256;
|
||||||
|
|
||||||
|
/// Holds the current state of the keyboard.
|
||||||
|
///
|
||||||
|
/// Must be explicitly updated each frame by calling `handle_event` each frame for all SDL2 events
|
||||||
|
/// received, as well as calling `do_events` once each frame. Usually, you would accomplish all
|
||||||
|
/// this house-keeping by simply calling [`System`]'s `do_events` method once per frame.
|
||||||
|
///
|
||||||
|
/// [`System`]: crate::System
|
||||||
|
pub struct Keyboard {
|
||||||
|
keyboard: [ButtonState; MAX_KEYS], // Box<[ButtonState]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Keyboard {
|
||||||
|
pub fn new() -> Keyboard {
|
||||||
|
Keyboard {
|
||||||
|
keyboard: [ButtonState::Idle; MAX_KEYS],
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
Keyboard {
|
||||||
|
keyboard: vec![ButtonState::Idle; 256].into_boxed_slice(),
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given key was just pressed or is being held down.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_key_down(&self, scancode: Scancode) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.keyboard[scancode as usize],
|
||||||
|
ButtonState::Pressed | ButtonState::Held
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given key was not just pressed and is not being held down.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_key_up(&self, scancode: Scancode) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.keyboard[scancode as usize],
|
||||||
|
ButtonState::Released | ButtonState::Idle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given key was just pressed (not being held down, yet).
|
||||||
|
#[inline]
|
||||||
|
pub fn is_key_pressed(&self, scancode: Scancode) -> bool {
|
||||||
|
self.keyboard[scancode as usize] == ButtonState::Pressed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given key was just released.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_key_released(&self, scancode: Scancode) -> bool {
|
||||||
|
self.keyboard[scancode as usize] == ButtonState::Released
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputDevice for Keyboard {
|
||||||
|
fn update(&mut self) {
|
||||||
|
for state in self.keyboard.iter_mut() {
|
||||||
|
*state = match *state {
|
||||||
|
ButtonState::Pressed => ButtonState::Held,
|
||||||
|
ButtonState::Released => ButtonState::Idle,
|
||||||
|
otherwise => otherwise,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(&mut self, event: &Event) {
|
||||||
|
match event {
|
||||||
|
Event::KeyDown { scancode, .. } => {
|
||||||
|
if let Some(scancode) = scancode {
|
||||||
|
let state = &mut self.keyboard[*scancode as usize];
|
||||||
|
*state = match *state {
|
||||||
|
ButtonState::Pressed => ButtonState::Held,
|
||||||
|
ButtonState::Held => ButtonState::Held,
|
||||||
|
_ => ButtonState::Pressed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::KeyUp { scancode, .. } => {
|
||||||
|
if let Some(scancode) = scancode {
|
||||||
|
self.keyboard[*scancode as usize] = ButtonState::Released;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
libretrogd/src/system/input_devices/mod.rs
Normal file
27
libretrogd/src/system/input_devices/mod.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
use sdl2::event::Event;
|
||||||
|
|
||||||
|
pub mod keyboard;
|
||||||
|
pub mod mouse;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Eq, PartialEq, Debug)]
|
||||||
|
pub enum ButtonState {
|
||||||
|
Idle,
|
||||||
|
Pressed,
|
||||||
|
Held,
|
||||||
|
Released,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Common trait for input device implementations.
|
||||||
|
pub trait InputDevice {
|
||||||
|
/// Performs internal house-keeping necessary for properly reporting the current state of this
|
||||||
|
/// input device. Normally this should be called on the device after all of this frame's
|
||||||
|
/// input events have been processed via `handle_event`.
|
||||||
|
fn update(&mut self);
|
||||||
|
|
||||||
|
/// Processes the data from the given [`Event`] if it is relevant for this input device. You
|
||||||
|
/// should pass in all events received every frame and let the input device figure out if it
|
||||||
|
/// is relevant to it or not.
|
||||||
|
///
|
||||||
|
/// [`Event`]: sdl2::event::Event
|
||||||
|
fn handle_event(&mut self, event: &Event);
|
||||||
|
}
|
318
libretrogd/src/system/input_devices/mouse.rs
Normal file
318
libretrogd/src/system/input_devices/mouse.rs
Normal file
|
@ -0,0 +1,318 @@
|
||||||
|
use sdl2::event::Event;
|
||||||
|
|
||||||
|
use crate::{Bitmap, BlitMethod, Rect};
|
||||||
|
|
||||||
|
use super::{ButtonState, InputDevice};
|
||||||
|
|
||||||
|
const MAX_BUTTONS: usize = 32;
|
||||||
|
|
||||||
|
const DEFAULT_MOUSE_CURSOR_HOTSPOT_X: u32 = 0;
|
||||||
|
const DEFAULT_MOUSE_CURSOR_HOTSPOT_Y: u32 = 0;
|
||||||
|
const DEFAULT_MOUSE_CURSOR_WIDTH: usize = 16;
|
||||||
|
const DEFAULT_MOUSE_CURSOR_HEIGHT: usize = 16;
|
||||||
|
|
||||||
|
#[rustfmt::skip]
|
||||||
|
const DEFAULT_MOUSE_CURSOR: [u8; DEFAULT_MOUSE_CURSOR_WIDTH * DEFAULT_MOUSE_CURSOR_HEIGHT] = [
|
||||||
|
0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x0f,0x0f,0x0f,0x0f,0x00,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x0f,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x0f,0x00,0x00,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0x00,0x00,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0xff,0xff,0xff,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0xff,0xff,0xff,0xff,0xff,0x00,0x0f,0x0f,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,
|
||||||
|
0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Holds the current state of the mouse.
|
||||||
|
///
|
||||||
|
/// Must be explicitly updated each frame by calling `handle_event` each frame for all SDL2 events
|
||||||
|
/// received, as well as calling `do_events` once each frame. Usually, you would accomplish all
|
||||||
|
/// this house-keeping by simply calling [`System`]'s `do_events` method once per frame.
|
||||||
|
///
|
||||||
|
/// [`System`]: crate::System
|
||||||
|
pub struct Mouse {
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
x_delta: i32,
|
||||||
|
y_delta: i32,
|
||||||
|
buttons: [ButtonState; MAX_BUTTONS],
|
||||||
|
cursor: Bitmap,
|
||||||
|
cursor_background: Bitmap,
|
||||||
|
cursor_hotspot_x: u32,
|
||||||
|
cursor_hotspot_y: u32,
|
||||||
|
cursor_enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Mouse {
|
||||||
|
pub fn new() -> Mouse {
|
||||||
|
let (cursor, cursor_background, cursor_hotspot_x, cursor_hotspot_y) =
|
||||||
|
Self::get_default_mouse_cursor();
|
||||||
|
|
||||||
|
Mouse {
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
x_delta: 0,
|
||||||
|
y_delta: 0,
|
||||||
|
buttons: [ButtonState::Idle; MAX_BUTTONS],
|
||||||
|
cursor,
|
||||||
|
cursor_background,
|
||||||
|
cursor_hotspot_x,
|
||||||
|
cursor_hotspot_y,
|
||||||
|
cursor_enabled: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current x coordinate of the mouse cursor.
|
||||||
|
#[inline]
|
||||||
|
pub fn x(&self) -> i32 {
|
||||||
|
self.x
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current y coordinate of the mouse cursor.
|
||||||
|
#[inline]
|
||||||
|
pub fn y(&self) -> i32 {
|
||||||
|
self.y
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the amount of pixels along the x-axis that the mouse cursor moved since the last
|
||||||
|
/// time that the mouse state was updated.
|
||||||
|
#[inline]
|
||||||
|
pub fn x_delta(&self) -> i32 {
|
||||||
|
self.x_delta
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the amount of pixels along the y-axis that the mouse cursor moved since the last
|
||||||
|
/// time that the mouse state was updated.
|
||||||
|
#[inline]
|
||||||
|
pub fn y_delta(&self) -> i32 {
|
||||||
|
self.y_delta
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given button was just pressed or is being held down.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_button_down(&self, button: usize) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.buttons[button],
|
||||||
|
ButtonState::Pressed | ButtonState::Held
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given button was not just pressed and is not being held down.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_button_up(&self, button: usize) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.buttons[button],
|
||||||
|
ButtonState::Released | ButtonState::Idle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given button was just pressed (not being held down, yet).
|
||||||
|
#[inline]
|
||||||
|
pub fn is_button_pressed(&self, button: usize) -> bool {
|
||||||
|
self.buttons[button] == ButtonState::Pressed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if the given button was just released.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_button_released(&self, button: usize) -> bool {
|
||||||
|
self.buttons[button] == ButtonState::Released
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a reference to the current mouse cursor bitmap.
|
||||||
|
#[inline]
|
||||||
|
pub fn cursor_bitmap(&self) -> &Bitmap {
|
||||||
|
&self.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current mouse cursor's "hotspot" x coordinate.
|
||||||
|
#[inline]
|
||||||
|
pub fn cursor_hotspot_x(&self) -> u32 {
|
||||||
|
self.cursor_hotspot_x
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current mouse cursor's "hotspot" y coordinate.
|
||||||
|
#[inline]
|
||||||
|
pub fn cursor_hotspot_y(&self) -> u32 {
|
||||||
|
self.cursor_hotspot_y
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if mouse cursor bitmap rendering is enabled.
|
||||||
|
#[inline]
|
||||||
|
pub fn is_cursor_enabled(&self) -> bool {
|
||||||
|
self.cursor_enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables or disables mouse cursor bitmap rendering.
|
||||||
|
#[inline]
|
||||||
|
pub fn enable_cursor(&mut self, enable: bool) {
|
||||||
|
self.cursor_enabled = enable;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the [`Bitmap`] used to display the mouse cursor and the "hotspot" coordinate. The
|
||||||
|
/// bitmap provided here should be set up to use color 255 as the transparent color.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
///
|
||||||
|
/// * `cursor`: the bitmap to be used to display the mouse cursor on screen
|
||||||
|
/// * `hotspot_x`: the "hotspot" x coordinate
|
||||||
|
/// * `hotspot_y`: the "hotspot" y coordinate.
|
||||||
|
pub fn set_mouse_cursor(&mut self, cursor: Bitmap, hotspot_x: u32, hotspot_y: u32) {
|
||||||
|
self.cursor = cursor;
|
||||||
|
self.cursor_background = Bitmap::new(self.cursor.width(), self.cursor.height()).unwrap();
|
||||||
|
self.cursor_hotspot_x = hotspot_x;
|
||||||
|
self.cursor_hotspot_y = hotspot_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resets the mouse cursor bitmap and "hotspot" coordinate back to the default settings.
|
||||||
|
pub fn set_default_mouse_cursor(&mut self) {
|
||||||
|
let (cursor, background, hotspot_x, hotspot_y) = Self::get_default_mouse_cursor();
|
||||||
|
self.cursor = cursor;
|
||||||
|
self.cursor_background = background;
|
||||||
|
self.cursor_hotspot_x = hotspot_x;
|
||||||
|
self.cursor_hotspot_y = hotspot_y;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_default_mouse_cursor() -> (Bitmap, Bitmap, u32, u32) {
|
||||||
|
let mut cursor = Bitmap::new(
|
||||||
|
DEFAULT_MOUSE_CURSOR_WIDTH as u32,
|
||||||
|
DEFAULT_MOUSE_CURSOR_HEIGHT as u32,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
cursor.pixels_mut().copy_from_slice(&DEFAULT_MOUSE_CURSOR);
|
||||||
|
|
||||||
|
let cursor_background = Bitmap::new(cursor.width(), cursor.height()).unwrap();
|
||||||
|
|
||||||
|
(
|
||||||
|
cursor,
|
||||||
|
cursor_background,
|
||||||
|
DEFAULT_MOUSE_CURSOR_HOTSPOT_X,
|
||||||
|
DEFAULT_MOUSE_CURSOR_HOTSPOT_Y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_cursor_render_position(&self) -> (i32, i32) {
|
||||||
|
(
|
||||||
|
self.x - self.cursor_hotspot_x as i32,
|
||||||
|
self.y - self.cursor_hotspot_y as i32,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders the mouse cursor bitmap onto the destination bitmap at the mouse's current
|
||||||
|
/// position. The destination bitmap specified is assumed to be the [`System`]'s video
|
||||||
|
/// backbuffer bitmap. The background on the destination bitmap is saved internally and a
|
||||||
|
/// subsequent call to [`Mouse::hide_cursor`] will restore the background.
|
||||||
|
///
|
||||||
|
/// If mouse cursor rendering is not currently enabled, this method does nothing.
|
||||||
|
///
|
||||||
|
/// Applications will not normally need to call this method, as if mouse cursor rendering is
|
||||||
|
/// enabled, this will be automatically handled by [`System::display`].
|
||||||
|
///
|
||||||
|
/// [`System`]: crate::System
|
||||||
|
/// [`System::display`]: crate::System::display
|
||||||
|
pub fn render_cursor(&mut self, dest: &mut Bitmap) {
|
||||||
|
if !self.cursor_enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x, y) = self.get_cursor_render_position();
|
||||||
|
|
||||||
|
// preserve existing background first
|
||||||
|
self.cursor_background.blit_region(
|
||||||
|
BlitMethod::Solid,
|
||||||
|
&dest,
|
||||||
|
&Rect::new(x, y, self.cursor.width(), self.cursor.height()),
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
|
||||||
|
dest.blit(BlitMethod::Transparent(255), &self.cursor, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Restores the original destination bitmap contents where the mouse cursor bitmap was
|
||||||
|
/// rendered to during the previous call to [`Mouse::render_cursor`]. The destination bitmap
|
||||||
|
/// specified is assumed to be the [`System`]'s video backbuffer bitmap.
|
||||||
|
///
|
||||||
|
/// If mouse cursor rendering is not currently enabled, this method does nothing.
|
||||||
|
///
|
||||||
|
/// Applications will not normally need to call this method, as if mouse cursor rendering is
|
||||||
|
/// enabled, this will be automatically handled by [`System::display`].
|
||||||
|
///
|
||||||
|
/// [`System`]: crate::System
|
||||||
|
/// [`System::display`]: crate::System::display
|
||||||
|
pub fn hide_cursor(&mut self, dest: &mut Bitmap) {
|
||||||
|
if !self.cursor_enabled {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x, y) = self.get_cursor_render_position();
|
||||||
|
dest.blit(BlitMethod::Solid, &self.cursor_background, x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_button_state(&mut self, button: u32, is_pressed: bool) {
|
||||||
|
let button_state = &mut self.buttons[button as usize];
|
||||||
|
*button_state = if is_pressed {
|
||||||
|
match *button_state {
|
||||||
|
ButtonState::Pressed => ButtonState::Held,
|
||||||
|
ButtonState::Held => ButtonState::Held,
|
||||||
|
_ => ButtonState::Pressed,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match *button_state {
|
||||||
|
ButtonState::Pressed | ButtonState::Held => ButtonState::Released,
|
||||||
|
ButtonState::Released => ButtonState::Idle,
|
||||||
|
ButtonState::Idle => ButtonState::Idle,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputDevice for Mouse {
|
||||||
|
fn update(&mut self) {
|
||||||
|
self.x_delta = 0;
|
||||||
|
self.y_delta = 0;
|
||||||
|
for state in self.buttons.iter_mut() {
|
||||||
|
*state = match *state {
|
||||||
|
ButtonState::Pressed => ButtonState::Held,
|
||||||
|
ButtonState::Released => ButtonState::Idle,
|
||||||
|
otherwise => otherwise,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_event(&mut self, event: &Event) {
|
||||||
|
match event {
|
||||||
|
Event::MouseMotion {
|
||||||
|
mousestate,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
xrel,
|
||||||
|
yrel,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.x = *x;
|
||||||
|
self.y = *y;
|
||||||
|
self.x_delta = *xrel;
|
||||||
|
self.y_delta = *yrel;
|
||||||
|
for (button, is_pressed) in mousestate.mouse_buttons() {
|
||||||
|
self.update_button_state(button as u32, is_pressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Event::MouseButtonDown { mouse_btn, .. } => {
|
||||||
|
self.update_button_state(*mouse_btn as u32, true);
|
||||||
|
}
|
||||||
|
Event::MouseButtonUp { mouse_btn, .. } => {
|
||||||
|
self.update_button_state(*mouse_btn as u32, false);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
400
libretrogd/src/system/mod.rs
Normal file
400
libretrogd/src/system/mod.rs
Normal file
|
@ -0,0 +1,400 @@
|
||||||
|
use byte_slice_cast::AsByteSlice;
|
||||||
|
use sdl2::event::Event;
|
||||||
|
use sdl2::pixels::PixelFormatEnum;
|
||||||
|
use sdl2::render::{Texture, WindowCanvas};
|
||||||
|
use sdl2::{EventPump, Sdl, TimerSubsystem, VideoSubsystem};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::{Bitmap, BitmaskFont, InputDevice, Keyboard, Mouse, Palette};
|
||||||
|
use crate::{DEFAULT_SCALE_FACTOR, SCREEN_HEIGHT, SCREEN_WIDTH};
|
||||||
|
|
||||||
|
pub mod input_devices;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum SystemError {
|
||||||
|
#[error("System init error: {0}")]
|
||||||
|
InitError(String),
|
||||||
|
|
||||||
|
#[error("System display error: {0}")]
|
||||||
|
DisplayError(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builder for configuring and constructing an instance of [`System`].
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SystemBuilder {
|
||||||
|
window_title: String,
|
||||||
|
vsync: bool,
|
||||||
|
target_framerate: Option<u32>,
|
||||||
|
initial_scale_factor: u32,
|
||||||
|
resizable: bool,
|
||||||
|
show_mouse: bool,
|
||||||
|
relative_mouse_scaling: bool,
|
||||||
|
integer_scaling: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemBuilder {
|
||||||
|
/// Returns a new [`SystemBuilder`] with a default configuration.
|
||||||
|
pub fn new() -> SystemBuilder {
|
||||||
|
SystemBuilder {
|
||||||
|
window_title: String::new(),
|
||||||
|
vsync: false,
|
||||||
|
target_framerate: None,
|
||||||
|
initial_scale_factor: DEFAULT_SCALE_FACTOR,
|
||||||
|
resizable: true,
|
||||||
|
show_mouse: false,
|
||||||
|
relative_mouse_scaling: true,
|
||||||
|
integer_scaling: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the window title for the [`System`] to be built.
|
||||||
|
pub fn window_title(&mut self, window_title: &str) -> &mut SystemBuilder {
|
||||||
|
self.window_title = window_title.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables or disables V-Sync for the [`System`] to be built. Enabling V-sync automatically
|
||||||
|
/// disables `target_framerate`.
|
||||||
|
pub fn vsync(&mut self, enable: bool) -> &mut SystemBuilder {
|
||||||
|
self.vsync = enable;
|
||||||
|
self.target_framerate = None;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets a target framerate for the [`System`] being built to run at. This is intended to be
|
||||||
|
/// used when V-sync is not desired, so setting a target framerate automatically disables
|
||||||
|
/// `vsync`.
|
||||||
|
pub fn target_framerate(&mut self, target_framerate: u32) -> &mut SystemBuilder {
|
||||||
|
self.target_framerate = Some(target_framerate);
|
||||||
|
self.vsync = false;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets an integer scaling factor for the [`System`] being built to up-scale the virtual
|
||||||
|
/// framebuffer to when displaying it on screen.
|
||||||
|
pub fn scale_factor(&mut self, scale_factor: u32) -> &mut SystemBuilder {
|
||||||
|
self.initial_scale_factor = scale_factor;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets whether the window will be resizable by the user for the [`System`] being built.
|
||||||
|
pub fn resizable(&mut self, enable: bool) -> &mut SystemBuilder {
|
||||||
|
self.resizable = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables or disables mouse cursor display by the operating system when the cursor is over
|
||||||
|
/// the window for the [`System`] being built. Disable this if you intend to render your own
|
||||||
|
/// custom mouse cursor.
|
||||||
|
pub fn show_mouse(&mut self, enable: bool) -> &mut SystemBuilder {
|
||||||
|
self.show_mouse = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables or disables automatic DPI scaling of mouse relative movement values (delta values)
|
||||||
|
/// available via the [`Mouse`] input device.
|
||||||
|
pub fn relative_mouse_scaling(&mut self, enable: bool) -> &mut SystemBuilder {
|
||||||
|
self.relative_mouse_scaling = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enables or disables restricting the final rendered output to always be integer scaled,
|
||||||
|
/// even if that result will not fully fill the area of the window.
|
||||||
|
pub fn integer_scaling(&mut self, enable: bool) -> &mut SystemBuilder {
|
||||||
|
self.integer_scaling = enable;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds and returns a [`System`] from the current configuration.
|
||||||
|
pub fn build(&self) -> Result<System, SystemError> {
|
||||||
|
// todo: maybe let this be customized in the future, or at least halved so a 160x120 mode can be available ... ?
|
||||||
|
let screen_width = SCREEN_WIDTH;
|
||||||
|
let screen_height = SCREEN_HEIGHT;
|
||||||
|
let texture_pixel_size = 4; // 32-bit ARGB format
|
||||||
|
|
||||||
|
sdl2::hint::set(
|
||||||
|
"SDL_MOUSE_RELATIVE_SCALING",
|
||||||
|
if self.relative_mouse_scaling {
|
||||||
|
"1"
|
||||||
|
} else {
|
||||||
|
"0"
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// build all the individual SDL subsystems
|
||||||
|
|
||||||
|
let sdl_context = match sdl2::init() {
|
||||||
|
Ok(sdl_context) => sdl_context,
|
||||||
|
Err(message) => return Err(SystemError::InitError(message)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sdl_timer_subsystem = match sdl_context.timer() {
|
||||||
|
Ok(timer_subsystem) => timer_subsystem,
|
||||||
|
Err(message) => return Err(SystemError::InitError(message)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sdl_video_subsystem = match sdl_context.video() {
|
||||||
|
Ok(video_subsystem) => video_subsystem,
|
||||||
|
Err(message) => return Err(SystemError::InitError(message)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let sdl_event_pump = match sdl_context.event_pump() {
|
||||||
|
Ok(event_pump) => event_pump,
|
||||||
|
Err(message) => return Err(SystemError::InitError(message)),
|
||||||
|
};
|
||||||
|
|
||||||
|
// create the window
|
||||||
|
|
||||||
|
let window_width = screen_width * self.initial_scale_factor;
|
||||||
|
let window_height = screen_height * self.initial_scale_factor;
|
||||||
|
let mut window_builder = &mut (sdl_video_subsystem.window(
|
||||||
|
self.window_title.as_str(),
|
||||||
|
window_width,
|
||||||
|
window_height,
|
||||||
|
));
|
||||||
|
if self.resizable {
|
||||||
|
window_builder = window_builder.resizable();
|
||||||
|
}
|
||||||
|
let sdl_window = match window_builder.build() {
|
||||||
|
Ok(window) => window,
|
||||||
|
Err(error) => return Err(SystemError::InitError(error.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
sdl_context.mouse().show_cursor(self.show_mouse);
|
||||||
|
|
||||||
|
// turn the window into a canvas (under the hood, an SDL Renderer that owns the window)
|
||||||
|
|
||||||
|
let mut canvas_builder = sdl_window.into_canvas();
|
||||||
|
if self.vsync {
|
||||||
|
canvas_builder = canvas_builder.present_vsync();
|
||||||
|
}
|
||||||
|
let mut sdl_canvas = match canvas_builder.build() {
|
||||||
|
Ok(canvas) => canvas,
|
||||||
|
Err(error) => return Err(SystemError::InitError(error.to_string())),
|
||||||
|
};
|
||||||
|
if let Err(error) = sdl_canvas.set_logical_size(screen_width, screen_height) {
|
||||||
|
return Err(SystemError::InitError(error.to_string()));
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: newer versions of rust-sdl2 support this directly off the WindowCanvas struct
|
||||||
|
unsafe {
|
||||||
|
sdl2::sys::SDL_RenderSetIntegerScale(
|
||||||
|
sdl_canvas.raw(),
|
||||||
|
if self.integer_scaling {
|
||||||
|
sdl2::sys::SDL_bool::SDL_TRUE
|
||||||
|
} else {
|
||||||
|
sdl2::sys::SDL_bool::SDL_FALSE
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// create an SDL texture which we will be uploading to every frame to display the
|
||||||
|
// application's framebuffer
|
||||||
|
|
||||||
|
let sdl_texture = match sdl_canvas.create_texture_streaming(
|
||||||
|
Some(PixelFormatEnum::ARGB8888),
|
||||||
|
screen_width,
|
||||||
|
screen_height,
|
||||||
|
) {
|
||||||
|
Ok(texture) => texture,
|
||||||
|
Err(error) => return Err(SystemError::InitError(error.to_string())),
|
||||||
|
};
|
||||||
|
let sdl_texture_pitch = (sdl_texture.query().width * texture_pixel_size) as usize;
|
||||||
|
|
||||||
|
// create a raw 32-bit RGBA buffer that will be used as the temporary source for
|
||||||
|
// SDL texture uploads each frame. necessary as applications are dealing with 8-bit indexed
|
||||||
|
// bitmaps, not 32-bit RGBA pixels, so this temporary buffer is where we convert the final
|
||||||
|
// application framebuffer to 32-bit RGBA pixels before it is uploaded to the SDL texture
|
||||||
|
let texture_pixels_size = (screen_width * screen_height * texture_pixel_size) as usize;
|
||||||
|
let texture_pixels = vec![0u32; texture_pixels_size].into_boxed_slice();
|
||||||
|
|
||||||
|
// create the Bitmap object that will be exposed to the application acting as the system
|
||||||
|
// backbuffer
|
||||||
|
|
||||||
|
let framebuffer = match Bitmap::new(SCREEN_WIDTH, SCREEN_HEIGHT) {
|
||||||
|
Ok(bmp) => bmp,
|
||||||
|
Err(error) => return Err(SystemError::InitError(error.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
// create the default palette, initialized to the VGA default palette. also exposed to the
|
||||||
|
// application for manipulation
|
||||||
|
|
||||||
|
let palette = match Palette::new_vga_palette() {
|
||||||
|
Ok(palette) => palette,
|
||||||
|
Err(error) => return Err(SystemError::InitError(error.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
// create the default font, initialized to the VGA BIOS default font.
|
||||||
|
|
||||||
|
let font = match BitmaskFont::new_vga_font() {
|
||||||
|
Ok(font) => font,
|
||||||
|
Err(error) => return Err(SystemError::InitError(error.to_string())),
|
||||||
|
};
|
||||||
|
|
||||||
|
// create input device objects, exposed to the application
|
||||||
|
|
||||||
|
let keyboard = Keyboard::new();
|
||||||
|
let mouse = Mouse::new();
|
||||||
|
|
||||||
|
Ok(System {
|
||||||
|
sdl_context,
|
||||||
|
sdl_video_subsystem,
|
||||||
|
sdl_timer_subsystem,
|
||||||
|
sdl_canvas,
|
||||||
|
sdl_texture,
|
||||||
|
sdl_texture_pitch,
|
||||||
|
sdl_event_pump,
|
||||||
|
texture_pixels,
|
||||||
|
video: framebuffer,
|
||||||
|
palette,
|
||||||
|
font,
|
||||||
|
keyboard,
|
||||||
|
mouse,
|
||||||
|
target_framerate: self.target_framerate,
|
||||||
|
target_framerate_delta: None,
|
||||||
|
next_tick: 0,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds all primary structures necessary for interacting with the operating system and for
|
||||||
|
/// applications to render to the display, react to input device events, etc. through the
|
||||||
|
/// "virtual machine" exposed by this library.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub struct System {
|
||||||
|
sdl_context: Sdl,
|
||||||
|
sdl_video_subsystem: VideoSubsystem,
|
||||||
|
sdl_timer_subsystem: TimerSubsystem,
|
||||||
|
sdl_canvas: WindowCanvas,
|
||||||
|
sdl_texture: Texture,
|
||||||
|
sdl_texture_pitch: usize,
|
||||||
|
sdl_event_pump: EventPump,
|
||||||
|
|
||||||
|
texture_pixels: Box<[u32]>,
|
||||||
|
|
||||||
|
target_framerate: Option<u32>,
|
||||||
|
target_framerate_delta: Option<i64>,
|
||||||
|
next_tick: i64,
|
||||||
|
|
||||||
|
/// The primary backbuffer [`Bitmap`] that will be rendered to the screen whenever
|
||||||
|
/// [`System::display`] is called. Regardless of the actual window size, this bitmap is always
|
||||||
|
/// [`SCREEN_WIDTH`]x[`SCREEN_HEIGHT`] pixels in size.
|
||||||
|
pub video: Bitmap,
|
||||||
|
|
||||||
|
/// The [`Palette`] that will be used in conjunction with the `video` backbuffer to
|
||||||
|
/// render the final output to the screen whenever [`System::display`] is called.
|
||||||
|
pub palette: Palette,
|
||||||
|
|
||||||
|
/// A pre-loaded [`Font`] that can be used for text rendering.
|
||||||
|
pub font: BitmaskFont,
|
||||||
|
|
||||||
|
/// The current keyboard state. To ensure it is updated each frame, you should call
|
||||||
|
/// [`System::do_events`] or [`System::do_events_with`] each frame.
|
||||||
|
pub keyboard: Keyboard,
|
||||||
|
|
||||||
|
/// The current mouse state. To ensure it is updated each frame, you should call
|
||||||
|
/// [`System::do_events`] or [`System::do_events_with`] each frame.
|
||||||
|
pub mouse: Mouse,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl System {
|
||||||
|
/// Takes the `video` backbuffer bitmap and `palette` and renders it to the window, up-scaled
|
||||||
|
/// to fill the window (preserving aspect ratio of course). If V-sync is enabled, this method
|
||||||
|
/// will block to wait for V-sync. Otherwise, if a target framerate was configured a delay
|
||||||
|
/// might be used to try to meet that framerate.
|
||||||
|
pub fn display(&mut self) -> Result<(), SystemError> {
|
||||||
|
self.mouse.render_cursor(&mut self.video);
|
||||||
|
|
||||||
|
// convert application framebuffer to 32-bit RGBA pixels, and then upload it to the SDL
|
||||||
|
// texture so it will be displayed on screen
|
||||||
|
|
||||||
|
self.video
|
||||||
|
.copy_as_argb_to(&mut self.texture_pixels, &self.palette);
|
||||||
|
|
||||||
|
let texture_pixels = self.texture_pixels.as_byte_slice();
|
||||||
|
if let Err(error) = self
|
||||||
|
.sdl_texture
|
||||||
|
.update(None, texture_pixels, self.sdl_texture_pitch)
|
||||||
|
{
|
||||||
|
return Err(SystemError::DisplayError(error.to_string()));
|
||||||
|
}
|
||||||
|
self.sdl_canvas.clear();
|
||||||
|
if let Err(error) = self.sdl_canvas.copy(&self.sdl_texture, None, None) {
|
||||||
|
return Err(SystemError::DisplayError(error));
|
||||||
|
}
|
||||||
|
self.sdl_canvas.present();
|
||||||
|
|
||||||
|
self.mouse.hide_cursor(&mut self.video);
|
||||||
|
|
||||||
|
// if a specific target framerate is desired, apply some loop timing/delay to achieve it
|
||||||
|
// TODO: do this better. delaying when running faster like this is a poor way to do this..
|
||||||
|
|
||||||
|
if let Some(target_framerate) = self.target_framerate {
|
||||||
|
if self.target_framerate_delta.is_some() {
|
||||||
|
// normal path for every other loop iteration except the first
|
||||||
|
let delay = self.next_tick - self.ticks() as i64;
|
||||||
|
if delay < 0 {
|
||||||
|
// this loop iteration took too long, no need to delay
|
||||||
|
self.next_tick -= delay;
|
||||||
|
} else {
|
||||||
|
// this loop iteration completed before next_tick time, delay by the remainder
|
||||||
|
// time period so we're running at about the desired framerate
|
||||||
|
self.delay(((delay * 1000) / self.tick_frequency() as i64) as u32);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// this branch will occur on the first main loop iteration. we use the fact that
|
||||||
|
// target_framerate_delta was not yet set to avoid doing any delay on the first
|
||||||
|
// loop, just in case there was some other processing between the System struct
|
||||||
|
// being created and the actual beginning of the first loop ...
|
||||||
|
self.target_framerate_delta =
|
||||||
|
Some((self.tick_frequency() / target_framerate as u64) as i64);
|
||||||
|
}
|
||||||
|
|
||||||
|
// expected time for the next display() call to happen by
|
||||||
|
self.next_tick = (self.ticks() as i64) + self.target_framerate_delta.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks for and responds to all SDL2 events waiting in the queue. Each event is passed to
|
||||||
|
/// all [`InputDevice`]'s automatically to ensure input device state is up to date.
|
||||||
|
pub fn do_events(&mut self) {
|
||||||
|
self.do_events_with(|_event| {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as [`System::do_events`] but also takes a function which will be called for each
|
||||||
|
/// SDL2 event being processed (after everything else has already processed it), allowing
|
||||||
|
/// your application to also react to any events received.
|
||||||
|
pub fn do_events_with<F>(&mut self, mut f: F)
|
||||||
|
where
|
||||||
|
F: FnMut(&Event),
|
||||||
|
{
|
||||||
|
self.keyboard.update();
|
||||||
|
self.mouse.update();
|
||||||
|
self.sdl_event_pump.pump_events();
|
||||||
|
for event in self.sdl_event_pump.poll_iter() {
|
||||||
|
self.keyboard.handle_event(&event);
|
||||||
|
self.mouse.handle_event(&event);
|
||||||
|
f(&event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ticks(&self) -> u64 {
|
||||||
|
self.sdl_timer_subsystem.performance_counter()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tick_frequency(&self) -> u64 {
|
||||||
|
self.sdl_timer_subsystem.performance_frequency()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of milliseconds elapsed since SDL was initialized.
|
||||||
|
pub fn millis(&self) -> u32 {
|
||||||
|
self.sdl_timer_subsystem.ticks()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Delays (blocks) for about the number of milliseconds specified.
|
||||||
|
pub fn delay(&mut self, millis: u32) {
|
||||||
|
self.sdl_timer_subsystem.delay(millis);
|
||||||
|
}
|
||||||
|
}
|
12
libretrogd/src/utils/bytes.rs
Normal file
12
libretrogd/src/utils/bytes.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
pub trait ReadFixedLengthByteArray {
|
||||||
|
fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N], std::io::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: std::io::Read> ReadFixedLengthByteArray for T {
|
||||||
|
fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N], std::io::Error> {
|
||||||
|
assert_ne!(N, 0);
|
||||||
|
let mut array = [0u8; N];
|
||||||
|
self.read_exact(&mut array)?;
|
||||||
|
Ok(array)
|
||||||
|
}
|
||||||
|
}
|
38
libretrogd/src/utils/mod.rs
Normal file
38
libretrogd/src/utils/mod.rs
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
use std::any::Any;
|
||||||
|
|
||||||
|
use num_traits::Unsigned;
|
||||||
|
use rand::distributions::uniform::SampleUniform;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
|
pub mod bytes;
|
||||||
|
pub mod packbits;
|
||||||
|
|
||||||
|
pub fn rnd_value<N: SampleUniform + PartialOrd>(low: N, high: N) -> N {
|
||||||
|
rand::thread_rng().gen_range(low..=high)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the absolute difference between two unsigned values. This is just here as a temporary
|
||||||
|
/// alternative to the `abs_diff` method currently provided by Rust but that is marked unstable.
|
||||||
|
#[inline]
|
||||||
|
pub fn abs_diff<N: Unsigned + PartialOrd>(a: N, b: N) -> N {
|
||||||
|
if a < b {
|
||||||
|
b - a
|
||||||
|
} else {
|
||||||
|
a - b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AsAny {
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<A: Any> AsAny for A {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self as &dyn Any
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_any_mut(&mut self) -> &mut dyn Any {
|
||||||
|
self as &mut dyn Any
|
||||||
|
}
|
||||||
|
}
|
226
libretrogd/src/utils/packbits.rs
Normal file
226
libretrogd/src/utils/packbits.rs
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum PackBitsError {
|
||||||
|
#[error("PackBits I/O error")]
|
||||||
|
IOError(#[from] std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PackMode {
|
||||||
|
Dump,
|
||||||
|
Run,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pack_bits<S, D>(src: &mut S, dest: &mut D, src_length: usize) -> Result<(), PackBitsError>
|
||||||
|
where
|
||||||
|
S: ReadBytesExt,
|
||||||
|
D: WriteBytesExt,
|
||||||
|
{
|
||||||
|
const MIN_RUN: usize = 3;
|
||||||
|
const MAX_RUN: usize = 128;
|
||||||
|
const MAX_BUFFER: usize = 128;
|
||||||
|
|
||||||
|
if src_length == 0 {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut bytes_left = src_length;
|
||||||
|
|
||||||
|
let mut buffer = [0u8; (MAX_RUN * 2)];
|
||||||
|
|
||||||
|
// read the first byte from the source, just to start things off before we get into the loop
|
||||||
|
buffer[0] = src.read_u8()?;
|
||||||
|
bytes_left -= 1;
|
||||||
|
|
||||||
|
let mut mode = PackMode::Dump;
|
||||||
|
let mut run_end = 1; // 1 because we already read the first byte into the buffer
|
||||||
|
let mut run_start = 0;
|
||||||
|
let mut previous_byte = buffer[0];
|
||||||
|
|
||||||
|
while bytes_left > 0 {
|
||||||
|
let byte = src.read_u8()?;
|
||||||
|
|
||||||
|
buffer[run_end] = byte;
|
||||||
|
run_end += 1;
|
||||||
|
|
||||||
|
match mode {
|
||||||
|
// "dump" mode. keep collecting any bytes and write them as-is until we detect the
|
||||||
|
// start of a run of identical bytes
|
||||||
|
PackMode::Dump => {
|
||||||
|
if run_end > MAX_BUFFER {
|
||||||
|
// we need to flush the temp buffer to the destination
|
||||||
|
dest.write_u8((run_end - 2) as u8)?;
|
||||||
|
dest.write_all(&buffer[0..run_end])?;
|
||||||
|
|
||||||
|
buffer[0] = byte;
|
||||||
|
run_end = 1;
|
||||||
|
run_start = 0;
|
||||||
|
} else if byte == previous_byte {
|
||||||
|
// detected the start of a run of identical bytes
|
||||||
|
if (run_end - run_start) >= MIN_RUN {
|
||||||
|
if run_start > 0 {
|
||||||
|
// we've found a run, flush the buffer we have currently so we can
|
||||||
|
// start tracking the length of this run
|
||||||
|
dest.write_u8((run_start - 1) as u8)?;
|
||||||
|
dest.write_all(&buffer[0..run_start])?;
|
||||||
|
}
|
||||||
|
mode = PackMode::Run;
|
||||||
|
} else if run_start == 0 {
|
||||||
|
mode = PackMode::Run;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
run_start = run_end - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// "run" mode. keep counting up bytes as long as they are identical to each other. when
|
||||||
|
// we find the end of a run, write out the run info and switch back to dump mode
|
||||||
|
PackMode::Run => {
|
||||||
|
// check for the end of a run of identical bytes
|
||||||
|
if (byte != previous_byte) || ((run_end - run_start) > MAX_RUN) {
|
||||||
|
// the identical byte run has ended, write it out to the destination
|
||||||
|
// (this is just two bytes, the count and the actual byte)
|
||||||
|
dest.write_i8(-((run_end - run_start - 2) as i8))?;
|
||||||
|
dest.write_u8(previous_byte)?;
|
||||||
|
|
||||||
|
// clear the temp buffer for our switch back to "dump" mode
|
||||||
|
buffer[0] = byte;
|
||||||
|
run_end = 1;
|
||||||
|
run_start = 0;
|
||||||
|
mode = PackMode::Dump;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
previous_byte = byte;
|
||||||
|
bytes_left -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// the source bytes have all been read, but we still might have to flush the temp buffer
|
||||||
|
// out to the destination, or finish writing out a run of identical bytes that was at the very
|
||||||
|
// end of the source
|
||||||
|
match mode {
|
||||||
|
PackMode::Dump => {
|
||||||
|
dest.write_u8((run_end - 1) as u8)?;
|
||||||
|
dest.write_all(&buffer[0..run_end])?;
|
||||||
|
}
|
||||||
|
PackMode::Run => {
|
||||||
|
dest.write_i8(-((run_end - run_start - 1) as i8))?;
|
||||||
|
dest.write_u8(previous_byte)?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unpack_bits<S, D>(
|
||||||
|
src: &mut S,
|
||||||
|
dest: &mut D,
|
||||||
|
unpacked_length: usize,
|
||||||
|
) -> Result<(), PackBitsError>
|
||||||
|
where
|
||||||
|
S: ReadBytesExt,
|
||||||
|
D: WriteBytesExt,
|
||||||
|
{
|
||||||
|
let mut buffer = [0u8; 128];
|
||||||
|
let mut bytes_written = 0;
|
||||||
|
|
||||||
|
while bytes_written < unpacked_length {
|
||||||
|
// read the next "code" byte that determines how to process the subsequent byte(s)
|
||||||
|
let byte = src.read_u8()?;
|
||||||
|
|
||||||
|
if byte > 128 {
|
||||||
|
// 129-255 = repeat the next byte 257-n times
|
||||||
|
let run_length = (257 - byte as u32) as usize;
|
||||||
|
|
||||||
|
// read the next byte from the source and repeat it the specified number of times
|
||||||
|
let byte = src.read_u8()?;
|
||||||
|
buffer.fill(byte);
|
||||||
|
dest.write_all(&buffer[0..run_length])?;
|
||||||
|
bytes_written += run_length;
|
||||||
|
} else if byte < 128 {
|
||||||
|
// 0-128 = copy next n-1 bytes from src to dest as-is
|
||||||
|
let run_length = (byte + 1) as usize;
|
||||||
|
|
||||||
|
src.read_exact(&mut buffer[0..run_length])?;
|
||||||
|
dest.write_all(&buffer[0..run_length])?;
|
||||||
|
bytes_written += run_length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// note that byte == 128 is a no-op (does it even appear in any files ???)
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
struct TestData<'a> {
|
||||||
|
packed: &'a [u8],
|
||||||
|
unpacked: &'a [u8],
|
||||||
|
}
|
||||||
|
|
||||||
|
static TEST_DATA: &[TestData] = &[
|
||||||
|
TestData {
|
||||||
|
packed: &[
|
||||||
|
0xfe, 0xaa, 0x02, 0x80, 0x00, 0x2a, 0xfd, 0xaa, 0x03, 0x80, 0x00, 0x2a, 0x22, 0xf7,
|
||||||
|
0xaa,
|
||||||
|
],
|
||||||
|
unpacked: &[
|
||||||
|
0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x2a, 0xaa, 0xaa, 0xaa, 0xaa, 0x80, 0x00, 0x2a, 0x22,
|
||||||
|
0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
packed: &[0x00, 0xaa],
|
||||||
|
unpacked: &[0xaa],
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
packed: &[0xf9, 0xaa],
|
||||||
|
unpacked: &[0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa],
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
packed: &[0xf9, 0xaa, 0x00, 0xbb],
|
||||||
|
unpacked: &[0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xbb],
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
packed: &[0x07, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8],
|
||||||
|
unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8],
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
packed: &[0x08, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8],
|
||||||
|
unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8],
|
||||||
|
},
|
||||||
|
TestData {
|
||||||
|
packed: &[0x06, 0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xfe, 0xa8],
|
||||||
|
unpacked: &[0xa0, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa8, 0xa8],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn packs() -> Result<(), PackBitsError> {
|
||||||
|
for TestData { packed, unpacked } in TEST_DATA {
|
||||||
|
let mut src = Cursor::new(*unpacked);
|
||||||
|
let mut dest = vec![0u8; 0];
|
||||||
|
pack_bits(&mut src, &mut dest, unpacked.len())?;
|
||||||
|
assert_eq!(dest, *packed);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unpacks() -> Result<(), PackBitsError> {
|
||||||
|
for TestData { packed, unpacked } in TEST_DATA {
|
||||||
|
let mut src = Cursor::new(*packed);
|
||||||
|
let mut dest = vec![0u8; 0];
|
||||||
|
unpack_bits(&mut src, &mut dest, unpacked.len())?;
|
||||||
|
assert_eq!(dest, *unpacked);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
BIN
libretrogd/test-assets/dp2.pal
Normal file
BIN
libretrogd/test-assets/dp2.pal
Normal file
Binary file not shown.
BIN
libretrogd/test-assets/test-tiles.lbm
Normal file
BIN
libretrogd/test-assets/test-tiles.lbm
Normal file
Binary file not shown.
BIN
libretrogd/test-assets/test.pcx
Executable file
BIN
libretrogd/test-assets/test.pcx
Executable file
Binary file not shown.
1
libretrogd/test-assets/test_bmp_pixels_raw.bin
Normal file
1
libretrogd/test-assets/test_bmp_pixels_raw.bin
Normal file
|
@ -0,0 +1 @@
|
||||||
|
~~~~~~~~~~~~~}}~}~}~}~~~}}~}~}~}}}|}}}|}|||}}||}|}|}}|{|||{|{|||{|{|{|{|||{z{z{z{z{z{zz{{z{{z{z{z{zyzzzyzzzzzzyzyzyzyzyzyyyyxyyyxyyyxyyyxyyxyxyxyxyxyxyxyxxxxxxxxxxxxxxxxx
|
BIN
libretrogd/test-assets/test_ilbm.lbm
Executable file
BIN
libretrogd/test-assets/test_ilbm.lbm
Executable file
Binary file not shown.
BIN
libretrogd/test-assets/test_image.lbm
Normal file
BIN
libretrogd/test-assets/test_image.lbm
Normal file
Binary file not shown.
BIN
libretrogd/test-assets/test_image.pcx
Normal file
BIN
libretrogd/test-assets/test_image.pcx
Normal file
Binary file not shown.
BIN
libretrogd/test-assets/test_pbm.lbm
Executable file
BIN
libretrogd/test-assets/test_pbm.lbm
Executable file
Binary file not shown.
Loading…
Reference in a new issue