use std::collections::HashMap;
use thiserror::Error;
use crate::models::{Composition, Palette, PaletteRef, Sprite, TransformSpec, Variant};
use crate::palette_parser::{PaletteParser, ParseMode};
use crate::palettes;
use crate::transforms::{self, Transform, TransformError};
pub trait Registry<V> {
fn contains(&self, name: &str) -> bool;
fn get(&self, name: &str) -> Option<&V>;
fn len(&self) -> usize;
fn is_empty(&self) -> bool {
self.len() == 0
}
fn clear(&mut self);
fn names(&self) -> Box<dyn Iterator<Item = &String> + '_>;
}
pub const MAGENTA_FALLBACK: &str = "#FF00FF";
#[derive(Debug, Clone, PartialEq)]
pub struct ResolvedPalette {
pub colors: HashMap<String, String>,
pub source: PaletteSource,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PaletteSource {
Named(String),
Builtin(String),
Inline,
Fallback,
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum PaletteError {
#[error("Palette '{0}' not found")]
NotFound(String),
#[error("Built-in palette '{0}' not found")]
BuiltinNotFound(String),
}
#[derive(Debug, Clone, PartialEq)]
pub struct PaletteWarning {
pub message: String,
}
impl PaletteWarning {
pub fn not_found(name: &str) -> Self {
Self { message: format!("Palette '{}' not found", name) }
}
pub fn builtin_not_found(name: &str) -> Self {
Self { message: format!("Built-in palette '{}' not found", name) }
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct LenientResult {
pub palette: ResolvedPalette,
pub warning: Option<PaletteWarning>,
}
fn resolve_palette_variables(
colors: &HashMap<String, String>,
strict: bool,
) -> (HashMap<String, String>, Vec<PaletteWarning>) {
let parser = PaletteParser::new();
let mode = if strict { ParseMode::Strict } else { ParseMode::Lenient };
match parser.resolve_to_strings(colors, mode) {
Ok(result) => {
let warnings: Vec<PaletteWarning> = result
.warnings
.into_iter()
.map(|w| PaletteWarning { message: w.message })
.collect();
(result.colors, warnings)
}
Err(e) => {
let mut warnings = Vec::new();
warnings.push(PaletteWarning { message: e.to_string() });
(colors.clone(), warnings)
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PaletteRegistry {
palettes: HashMap<String, Palette>,
}
impl PaletteRegistry {
pub fn new() -> Self {
Self { palettes: HashMap::new() }
}
pub fn register(&mut self, palette: Palette) {
self.palettes.insert(palette.name.clone(), palette);
}
pub fn get(&self, name: &str) -> Option<&Palette> {
self.palettes.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.palettes.contains_key(name)
}
pub fn resolve_strict(&self, sprite: &Sprite) -> Result<ResolvedPalette, PaletteError> {
match &sprite.palette {
PaletteRef::Named(name) => {
if let Some(builtin_name) = name.strip_prefix('@') {
if let Some(palette) = palettes::get_builtin(builtin_name) {
Ok(ResolvedPalette {
colors: palette.colors.clone(),
source: PaletteSource::Builtin(builtin_name.to_string()),
})
} else {
Err(PaletteError::BuiltinNotFound(builtin_name.to_string()))
}
} else if let Some(palette) = self.palettes.get(name) {
let (resolved_colors, _warnings) =
resolve_palette_variables(&palette.colors, true);
Ok(ResolvedPalette {
colors: resolved_colors,
source: PaletteSource::Named(name.clone()),
})
} else {
Err(PaletteError::NotFound(name.clone()))
}
}
PaletteRef::Inline(colors) => {
let (resolved_colors, _warnings) = resolve_palette_variables(colors, true);
Ok(ResolvedPalette { colors: resolved_colors, source: PaletteSource::Inline })
}
}
}
pub fn resolve_lenient(&self, sprite: &Sprite) -> LenientResult {
match &sprite.palette {
PaletteRef::Named(name) => {
if let Some(builtin_name) = name.strip_prefix('@') {
if let Some(palette) = palettes::get_builtin(builtin_name) {
LenientResult {
palette: ResolvedPalette {
colors: palette.colors.clone(),
source: PaletteSource::Builtin(builtin_name.to_string()),
},
warning: None,
}
} else {
LenientResult {
palette: ResolvedPalette {
colors: HashMap::new(),
source: PaletteSource::Fallback,
},
warning: Some(PaletteWarning::builtin_not_found(builtin_name)),
}
}
} else if let Some(palette) = self.palettes.get(name) {
let (resolved_colors, var_warnings) =
resolve_palette_variables(&palette.colors, false);
let warning = if var_warnings.is_empty() {
None
} else {
let messages: Vec<String> =
var_warnings.into_iter().map(|w| w.message).collect();
Some(PaletteWarning { message: messages.join("; ") })
};
LenientResult {
palette: ResolvedPalette {
colors: resolved_colors,
source: PaletteSource::Named(name.clone()),
},
warning,
}
} else {
LenientResult {
palette: ResolvedPalette {
colors: HashMap::new(),
source: PaletteSource::Fallback,
},
warning: Some(PaletteWarning::not_found(name)),
}
}
}
PaletteRef::Inline(colors) => {
let (resolved_colors, var_warnings) = resolve_palette_variables(colors, false);
let warning = if var_warnings.is_empty() {
None
} else {
let messages: Vec<String> =
var_warnings.into_iter().map(|w| w.message).collect();
Some(PaletteWarning { message: messages.join("; ") })
};
LenientResult {
palette: ResolvedPalette {
colors: resolved_colors,
source: PaletteSource::Inline,
},
warning,
}
}
}
}
pub fn resolve(&self, sprite: &Sprite, strict: bool) -> Result<LenientResult, PaletteError> {
if strict {
self.resolve_strict(sprite).map(|palette| LenientResult { palette, warning: None })
} else {
Ok(self.resolve_lenient(sprite))
}
}
pub fn len(&self) -> usize {
self.palettes.len()
}
pub fn is_empty(&self) -> bool {
self.palettes.is_empty()
}
pub fn clear(&mut self) {
self.palettes.clear();
}
pub fn names(&self) -> impl Iterator<Item = &String> {
self.palettes.keys()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &Palette)> {
self.palettes.iter()
}
}
impl Registry<Palette> for PaletteRegistry {
fn contains(&self, name: &str) -> bool {
self.palettes.contains_key(name)
}
fn get(&self, name: &str) -> Option<&Palette> {
self.palettes.get(name)
}
fn len(&self) -> usize {
self.palettes.len()
}
fn clear(&mut self) {
self.palettes.clear();
}
fn names(&self) -> Box<dyn Iterator<Item = &String> + '_> {
Box::new(self.palettes.keys())
}
}
#[derive(Debug, Clone, PartialEq, Error)]
pub enum SpriteError {
#[error("Sprite or variant '{0}' not found")]
NotFound(String),
#[error("Variant '{variant}' references unknown base sprite '{base}'")]
BaseNotFound { variant: String, base: String },
#[error("Sprite '{sprite}' references unknown source sprite '{source_name}'")]
SourceNotFound { sprite: String, source_name: String },
#[error("Circular reference detected for sprite '{sprite}': {}", chain.join(" -> "))]
CircularReference { sprite: String, chain: Vec<String> },
#[error("Transform error for sprite '{sprite}': {message}")]
TransformError { sprite: String, message: String },
}
#[derive(Debug, Clone, PartialEq)]
pub struct SpriteWarning {
pub message: String,
}
impl SpriteWarning {
pub fn not_found(name: &str) -> Self {
Self { message: format!("Sprite or variant '{}' not found", name) }
}
pub fn base_not_found(variant: &str, base: &str) -> Self {
Self { message: format!("Variant '{}' references unknown base sprite '{}'", variant, base) }
}
pub fn source_not_found(sprite: &str, source: &str) -> Self {
Self {
message: format!("Sprite '{}' references unknown source sprite '{}'", sprite, source),
}
}
pub fn transform_error(sprite: &str, message: &str) -> Self {
Self { message: format!("Transform error for sprite '{}': {}", sprite, message) }
}
}
#[derive(Debug, Clone)]
pub struct ResolvedSprite {
pub name: String,
pub size: Option<[u32; 2]>,
pub grid: Vec<String>,
pub palette: HashMap<String, String>,
pub warnings: Vec<SpriteWarning>,
pub nine_slice: Option<crate::models::NineSlice>,
}
fn parse_transform_spec(spec: &TransformSpec) -> Result<Transform, TransformError> {
match spec {
TransformSpec::String(s) => transforms::parse_transform_str(s),
TransformSpec::Object { op, params } => {
let mut obj = serde_json::Map::new();
obj.insert("op".to_string(), serde_json::Value::String(op.clone()));
for (k, v) in params {
obj.insert(k.clone(), v.clone());
}
transforms::parse_transform_value(&serde_json::Value::Object(obj))
}
}
}
fn apply_grid_transform(
grid: &[String],
transform: &Transform,
) -> Result<Vec<String>, TransformError> {
match transform {
Transform::MirrorH => Ok(mirror_horizontal(grid)),
Transform::MirrorV => Ok(mirror_vertical(grid)),
Transform::Rotate { degrees } => rotate_grid(grid, *degrees),
Transform::Tile { w, h } => Ok(tile_grid(grid, *w, *h)),
Transform::Pad { size } => Ok(pad_grid(grid, *size)),
Transform::Crop { x, y, w, h } => crop_grid(grid, *x, *y, *w, *h),
Transform::Outline { token, width } => Ok(outline_grid(grid, token.as_deref(), *width)),
Transform::Shift { x, y } => Ok(shift_grid(grid, *x, *y)),
Transform::Shadow { x, y, token } => Ok(shadow_grid(grid, *x, *y, token.as_deref())),
Transform::SelOut { fallback, mapping } => {
Ok(transforms::apply_selout(grid, fallback.as_deref(), mapping.as_ref()))
}
Transform::Scale { x, y } => Ok(transforms::apply_scale(grid, *x, *y)),
Transform::Dither { .. } | Transform::DitherGradient { .. } => {
Err(TransformError::InvalidParameter {
op: "dither".to_string(),
message: "dither transforms are not yet implemented for sprite grids".to_string(),
})
}
Transform::Subpixel { .. } => Err(TransformError::InvalidParameter {
op: "subpixel".to_string(),
message: "subpixel transforms are not yet implemented for sprite grids".to_string(),
}),
Transform::Pingpong { .. }
| Transform::Reverse
| Transform::FrameOffset { .. }
| Transform::Hold { .. } => Err(TransformError::InvalidParameter {
op: format!("{:?}", transform),
message: "animation transforms cannot be applied to sprite grids".to_string(),
}),
}
}
fn mirror_horizontal(grid: &[String]) -> Vec<String> {
use crate::tokenizer::tokenize;
grid.iter()
.map(|row| {
let (tokens, _) = tokenize(row);
tokens.into_iter().rev().collect::<Vec<_>>().join("")
})
.collect()
}
fn mirror_vertical(grid: &[String]) -> Vec<String> {
grid.iter().rev().cloned().collect()
}
fn rotate_grid(grid: &[String], degrees: u16) -> Result<Vec<String>, TransformError> {
use crate::tokenizer::tokenize;
if grid.is_empty() {
return Ok(Vec::new());
}
let parsed: Vec<Vec<String>> = grid.iter().map(|row| tokenize(row).0).collect();
let height = parsed.len();
let width = parsed.iter().map(|r| r.len()).max().unwrap_or(0);
if width == 0 {
return Ok(grid.to_vec());
}
let padded: Vec<Vec<String>> = parsed
.into_iter()
.map(|mut row| {
while row.len() < width {
row.push("{_}".to_string());
}
row
})
.collect();
match degrees {
90 => {
let mut result = vec![vec![String::new(); height]; width];
for (row, tokens) in padded.iter().enumerate() {
for (col, token) in tokens.iter().enumerate() {
result[col][height - 1 - row] = token.clone();
}
}
Ok(result.into_iter().map(|row| row.join("")).collect())
}
180 => {
Ok(mirror_vertical(&mirror_horizontal(grid)))
}
270 => {
let mut result = vec![vec![String::new(); height]; width];
for (row, tokens) in padded.iter().enumerate() {
for (col, token) in tokens.iter().enumerate() {
result[width - 1 - col][row] = token.clone();
}
}
Ok(result.into_iter().map(|row| row.join("")).collect())
}
_ => Err(TransformError::InvalidRotation(degrees)),
}
}
fn tile_grid(grid: &[String], w: u32, h: u32) -> Vec<String> {
if grid.is_empty() || w == 0 || h == 0 {
return Vec::new();
}
let mut result = Vec::new();
for _ in 0..h {
for row in grid {
result.push(row.repeat(w as usize));
}
}
result
}
fn pad_grid(grid: &[String], size: u32) -> Vec<String> {
use crate::tokenizer::tokenize;
if grid.is_empty() || size == 0 {
return grid.to_vec();
}
let parsed: Vec<Vec<String>> = grid.iter().map(|row| tokenize(row).0).collect();
let max_width = parsed.iter().map(|r| r.len()).max().unwrap_or(0);
let pad_token = "{_}";
let horizontal_padding: String =
std::iter::repeat_n(pad_token, size as usize).collect::<Vec<_>>().join("");
let full_width_row: String =
std::iter::repeat_n(pad_token, max_width + 2 * size as usize).collect::<Vec<_>>().join("");
let mut result = Vec::new();
for _ in 0..size {
result.push(full_width_row.clone());
}
for row in &parsed {
let mut padded_row = row.clone();
while padded_row.len() < max_width {
padded_row.push(pad_token.to_string());
}
let content = padded_row.join("");
result.push(format!("{}{}{}", horizontal_padding, content, horizontal_padding));
}
for _ in 0..size {
result.push(full_width_row.clone());
}
result
}
fn crop_grid(
grid: &[String],
x: u32,
y: u32,
w: u32,
h: u32,
) -> Result<Vec<String>, TransformError> {
use crate::tokenizer::tokenize;
if grid.is_empty() {
return Ok(Vec::new());
}
let parsed: Vec<Vec<String>> = grid.iter().map(|row| tokenize(row).0).collect();
let grid_height = parsed.len();
let grid_width = parsed.iter().map(|r| r.len()).max().unwrap_or(0);
if y as usize >= grid_height || x as usize >= grid_width {
return Err(TransformError::InvalidCropRegion(format!(
"crop origin ({}, {}) is outside grid bounds ({}x{})",
x, y, grid_width, grid_height
)));
}
let mut result = Vec::new();
for row_idx in y..(y + h) {
if row_idx as usize >= parsed.len() {
let transparent_row: String =
std::iter::repeat_n("{_}", w as usize).collect::<Vec<_>>().join("");
result.push(transparent_row);
} else {
let row = &parsed[row_idx as usize];
let mut cropped_row = Vec::new();
for col_idx in x..(x + w) {
if (col_idx as usize) < row.len() {
cropped_row.push(row[col_idx as usize].clone());
} else {
cropped_row.push("{_}".to_string());
}
}
result.push(cropped_row.join(""));
}
}
Ok(result)
}
fn outline_grid(grid: &[String], token: Option<&str>, width: u32) -> Vec<String> {
use crate::tokenizer::tokenize;
if grid.is_empty() || width == 0 {
return grid.to_vec();
}
let outline_token = token.unwrap_or("{outline}");
let transparent = "{_}";
let parsed: Vec<Vec<String>> = grid.iter().map(|row| tokenize(row).0).collect();
let height = parsed.len();
let width_pixels = parsed.iter().map(|r| r.len()).max().unwrap_or(0);
if width_pixels == 0 {
return grid.to_vec();
}
let padded: Vec<Vec<String>> = parsed
.into_iter()
.map(|mut row| {
while row.len() < width_pixels {
row.push(transparent.to_string());
}
row
})
.collect();
let new_width = width_pixels + 2 * width as usize;
let new_height = height + 2 * width as usize;
let mut result: Vec<Vec<String>> = vec![vec![transparent.to_string(); new_width]; new_height];
for (y, row) in padded.iter().enumerate() {
for (x, token_val) in row.iter().enumerate() {
result[y + width as usize][x + width as usize] = token_val.clone();
}
}
for y in 0..height {
for x in 0..width_pixels {
let token_val = &padded[y][x];
if token_val != transparent {
let out_y = y + width as usize;
let out_x = x + width as usize;
for dy in -(width as i32)..=(width as i32) {
for dx in -(width as i32)..=(width as i32) {
if dy == 0 && dx == 0 {
continue; }
let ny = (out_y as i32 + dy) as usize;
let nx = (out_x as i32 + dx) as usize;
if result[ny][nx] == transparent {
result[ny][nx] = outline_token.to_string();
}
}
}
}
}
}
result.into_iter().map(|row| row.join("")).collect()
}
fn shift_grid(grid: &[String], x: i32, y: i32) -> Vec<String> {
use crate::tokenizer::tokenize;
if grid.is_empty() {
return Vec::new();
}
let parsed: Vec<Vec<String>> = grid.iter().map(|row| tokenize(row).0).collect();
let height = parsed.len();
let width = parsed.iter().map(|r| r.len()).max().unwrap_or(0);
if width == 0 {
return grid.to_vec();
}
let padded: Vec<Vec<String>> = parsed
.into_iter()
.map(|mut row| {
while row.len() < width {
row.push("{_}".to_string());
}
row
})
.collect();
let shift_y = ((y % height as i32) + height as i32) as usize % height;
let shift_x = ((x % width as i32) + width as i32) as usize % width;
let mut shifted_rows: Vec<Vec<String>> = Vec::with_capacity(height);
for i in 0..height {
let src_y = (i + height - shift_y) % height;
shifted_rows.push(padded[src_y].clone());
}
let result: Vec<String> = shifted_rows
.into_iter()
.map(|row| {
let mut shifted_row: Vec<String> = Vec::with_capacity(width);
for i in 0..width {
let src_x = (i + width - shift_x) % width;
shifted_row.push(row[src_x].clone());
}
shifted_row.join("")
})
.collect();
result
}
fn shadow_grid(grid: &[String], x: i32, y: i32, token: Option<&str>) -> Vec<String> {
use crate::tokenizer::tokenize;
if grid.is_empty() {
return Vec::new();
}
let shadow_token = token.unwrap_or("{shadow}");
let transparent = "{_}";
let parsed: Vec<Vec<String>> = grid.iter().map(|row| tokenize(row).0).collect();
let height = parsed.len();
let width = parsed.iter().map(|r| r.len()).max().unwrap_or(0);
if width == 0 {
return grid.to_vec();
}
let padded: Vec<Vec<String>> = parsed
.into_iter()
.map(|mut row| {
while row.len() < width {
row.push(transparent.to_string());
}
row
})
.collect();
let extra_left = if x < 0 { (-x) as usize } else { 0 };
let extra_right = if x > 0 { x as usize } else { 0 };
let extra_top = if y < 0 { (-y) as usize } else { 0 };
let extra_bottom = if y > 0 { y as usize } else { 0 };
let new_width = width + extra_left + extra_right;
let new_height = height + extra_top + extra_bottom;
let mut result: Vec<Vec<String>> = vec![vec![transparent.to_string(); new_width]; new_height];
for (row_y, row) in padded.iter().enumerate() {
for (col_x, token_val) in row.iter().enumerate() {
if token_val != transparent {
let shadow_y = (row_y as i32 + extra_top as i32 + y) as usize;
let shadow_x = (col_x as i32 + extra_left as i32 + x) as usize;
if shadow_y < new_height && shadow_x < new_width {
result[shadow_y][shadow_x] = shadow_token.to_string();
}
}
}
}
for (row_y, row) in padded.iter().enumerate() {
for (col_x, token_val) in row.iter().enumerate() {
let out_y = row_y + extra_top;
let out_x = col_x + extra_left;
if token_val != transparent {
result[out_y][out_x] = token_val.clone();
}
}
}
result.into_iter().map(|row| row.join("")).collect()
}
#[derive(Debug, Clone, Default)]
pub struct SpriteRegistry {
sprites: HashMap<String, Sprite>,
variants: HashMap<String, Variant>,
}
impl SpriteRegistry {
pub fn new() -> Self {
Self { sprites: HashMap::new(), variants: HashMap::new() }
}
pub fn register_sprite(&mut self, sprite: Sprite) {
self.sprites.insert(sprite.name.clone(), sprite);
}
pub fn register_variant(&mut self, variant: Variant) {
self.variants.insert(variant.name.clone(), variant);
}
pub fn get_sprite(&self, name: &str) -> Option<&Sprite> {
self.sprites.get(name)
}
pub fn get_variant(&self, name: &str) -> Option<&Variant> {
self.variants.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.sprites.contains_key(name) || self.variants.contains_key(name)
}
pub fn resolve(
&self,
name: &str,
palette_registry: &PaletteRegistry,
strict: bool,
) -> Result<ResolvedSprite, SpriteError> {
if let Some(sprite) = self.sprites.get(name) {
return self.resolve_sprite(sprite, palette_registry, strict);
}
if let Some(variant) = self.variants.get(name) {
return self.resolve_variant(variant, palette_registry, strict);
}
if strict {
Err(SpriteError::NotFound(name.to_string()))
} else {
Ok(ResolvedSprite {
name: name.to_string(),
size: None,
grid: vec![],
palette: HashMap::new(),
warnings: vec![SpriteWarning::not_found(name)],
nine_slice: None,
})
}
}
fn resolve_sprite(
&self,
sprite: &Sprite,
palette_registry: &PaletteRegistry,
strict: bool,
) -> Result<ResolvedSprite, SpriteError> {
self.resolve_sprite_internal(sprite, palette_registry, strict, &mut Vec::new())
}
fn resolve_sprite_internal(
&self,
sprite: &Sprite,
palette_registry: &PaletteRegistry,
strict: bool,
visited: &mut Vec<String>,
) -> Result<ResolvedSprite, SpriteError> {
let mut warnings = Vec::new();
if visited.contains(&sprite.name) {
visited.push(sprite.name.clone());
if strict {
return Err(SpriteError::CircularReference {
sprite: sprite.name.clone(),
chain: visited.clone(),
});
} else {
return Ok(ResolvedSprite {
name: sprite.name.clone(),
size: None,
grid: vec![],
palette: HashMap::new(),
warnings: vec![SpriteWarning {
message: format!("Circular reference detected: {}", visited.join(" -> ")),
}],
nine_slice: None,
});
}
}
visited.push(sprite.name.clone());
let base_grid = if let Some(source_name) = &sprite.source {
match self.sprites.get(source_name) {
Some(source_sprite) => {
let source_resolved = self.resolve_sprite_internal(
source_sprite,
palette_registry,
strict,
visited,
)?;
warnings.extend(source_resolved.warnings);
source_resolved.grid
}
None => {
if strict {
return Err(SpriteError::SourceNotFound {
sprite: sprite.name.clone(),
source_name: source_name.clone(),
});
} else {
warnings.push(SpriteWarning::source_not_found(&sprite.name, source_name));
Vec::new()
}
}
}
} else {
sprite.grid.clone()
};
let grid = if let Some(transform_specs) = &sprite.transform {
match self.apply_transforms_to_grid(&base_grid, transform_specs, &sprite.name, strict) {
Ok((transformed, transform_warnings)) => {
warnings.extend(transform_warnings);
transformed
}
Err(e) => {
if strict {
return Err(e);
} else {
warnings.push(SpriteWarning::transform_error(&sprite.name, &e.to_string()));
base_grid
}
}
}
} else {
base_grid
};
let palette = match palette_registry.resolve(sprite, strict) {
Ok(result) => {
if let Some(warning) = result.warning {
warnings.push(SpriteWarning { message: warning.message });
}
result.palette.colors
}
Err(e) => {
if strict {
return Err(SpriteError::NotFound(format!("palette error: {}", e)));
}
HashMap::new()
}
};
Ok(ResolvedSprite {
name: sprite.name.clone(),
size: sprite.size,
grid,
palette,
warnings,
nine_slice: sprite.nine_slice.clone(),
})
}
fn apply_transforms_to_grid(
&self,
grid: &[String],
transform_specs: &[TransformSpec],
sprite_name: &str,
strict: bool,
) -> Result<(Vec<String>, Vec<SpriteWarning>), SpriteError> {
let mut warnings = Vec::new();
let mut current_grid = grid.to_vec();
for spec in transform_specs {
let transform = match parse_transform_spec(spec) {
Ok(t) => t,
Err(e) => {
if strict {
return Err(SpriteError::TransformError {
sprite: sprite_name.to_string(),
message: e.to_string(),
});
} else {
warnings.push(SpriteWarning::transform_error(sprite_name, &e.to_string()));
continue;
}
}
};
if transforms::is_animation_transform(&transform) {
warnings.push(SpriteWarning::transform_error(
sprite_name,
&format!("{:?} is an animation-only transform", transform),
));
continue;
}
match apply_grid_transform(¤t_grid, &transform) {
Ok(new_grid) => {
current_grid = new_grid;
}
Err(e) => {
if strict {
return Err(SpriteError::TransformError {
sprite: sprite_name.to_string(),
message: e.to_string(),
});
} else {
warnings.push(SpriteWarning::transform_error(sprite_name, &e.to_string()));
}
}
}
}
Ok((current_grid, warnings))
}
fn resolve_variant(
&self,
variant: &Variant,
palette_registry: &PaletteRegistry,
strict: bool,
) -> Result<ResolvedSprite, SpriteError> {
let base_sprite = match self.sprites.get(&variant.base) {
Some(sprite) => sprite,
None => {
if strict {
return Err(SpriteError::BaseNotFound {
variant: variant.name.clone(),
base: variant.base.clone(),
});
} else {
return Ok(ResolvedSprite {
name: variant.name.clone(),
size: None,
grid: vec![],
palette: HashMap::new(),
warnings: vec![SpriteWarning::base_not_found(&variant.name, &variant.base)],
nine_slice: None,
});
}
}
};
let mut warnings = Vec::new();
let base_palette = match palette_registry.resolve(base_sprite, strict) {
Ok(result) => {
if let Some(warning) = result.warning {
warnings.push(SpriteWarning { message: warning.message });
}
result.palette.colors
}
Err(e) => {
if strict {
return Err(SpriteError::NotFound(format!("base palette error: {}", e)));
}
HashMap::new()
}
};
let mut merged_palette = base_palette;
for (token, color) in &variant.palette {
merged_palette.insert(token.clone(), color.clone());
}
let base_grid = base_sprite.grid.clone();
let grid = if let Some(transform_specs) = &variant.transform {
match self.apply_transforms_to_grid(&base_grid, transform_specs, &variant.name, strict)
{
Ok((transformed, transform_warnings)) => {
warnings.extend(transform_warnings);
transformed
}
Err(e) => {
if strict {
return Err(e);
} else {
warnings
.push(SpriteWarning::transform_error(&variant.name, &e.to_string()));
base_grid
}
}
}
} else {
base_grid
};
Ok(ResolvedSprite {
name: variant.name.clone(),
size: base_sprite.size,
grid,
palette: merged_palette,
warnings,
nine_slice: base_sprite.nine_slice.clone(),
})
}
pub fn names(&self) -> impl Iterator<Item = &String> {
self.sprites.keys().chain(self.variants.keys())
}
pub fn len(&self) -> usize {
self.sprites.len() + self.variants.len()
}
pub fn is_empty(&self) -> bool {
self.sprites.is_empty() && self.variants.is_empty()
}
pub fn clear(&mut self) {
self.sprites.clear();
self.variants.clear();
}
pub fn sprite_count(&self) -> usize {
self.sprites.len()
}
pub fn variant_count(&self) -> usize {
self.variants.len()
}
pub fn sprites(&self) -> impl Iterator<Item = (&String, &Sprite)> {
self.sprites.iter()
}
pub fn variants(&self) -> impl Iterator<Item = (&String, &Variant)> {
self.variants.iter()
}
}
impl Registry<Sprite> for SpriteRegistry {
fn contains(&self, name: &str) -> bool {
self.sprites.contains_key(name)
}
fn get(&self, name: &str) -> Option<&Sprite> {
self.sprites.get(name)
}
fn len(&self) -> usize {
self.sprites.len()
}
fn clear(&mut self) {
self.sprites.clear();
}
fn names(&self) -> Box<dyn Iterator<Item = &String> + '_> {
Box::new(self.sprites.keys())
}
}
impl Registry<Variant> for SpriteRegistry {
fn contains(&self, name: &str) -> bool {
self.variants.contains_key(name)
}
fn get(&self, name: &str) -> Option<&Variant> {
self.variants.get(name)
}
fn len(&self) -> usize {
self.variants.len()
}
fn clear(&mut self) {
self.variants.clear();
}
fn names(&self) -> Box<dyn Iterator<Item = &String> + '_> {
Box::new(self.variants.keys())
}
}
use crate::models::TransformDef;
#[derive(Debug, Clone, Default)]
pub struct TransformRegistry {
transforms: HashMap<String, TransformDef>,
}
impl TransformRegistry {
pub fn new() -> Self {
Self { transforms: HashMap::new() }
}
pub fn register(&mut self, transform: TransformDef) {
self.transforms.insert(transform.name.clone(), transform);
}
pub fn get(&self, name: &str) -> Option<&TransformDef> {
self.transforms.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.transforms.contains_key(name)
}
pub fn len(&self) -> usize {
self.transforms.len()
}
pub fn is_empty(&self) -> bool {
self.transforms.is_empty()
}
pub fn clear(&mut self) {
self.transforms.clear();
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &TransformDef)> {
self.transforms.iter()
}
pub fn expand(
&self,
name: &str,
params: &HashMap<String, f64>,
frame: u32,
total_frames: u32,
) -> Result<Vec<Transform>, TransformError> {
let transform_def = self
.transforms
.get(name)
.ok_or_else(|| TransformError::UnknownOperation(name.to_string()))?;
transforms::generate_frame_transforms(transform_def, frame, total_frames, params)
}
pub fn expand_simple(
&self,
name: &str,
params: &HashMap<String, f64>,
) -> Result<Vec<Transform>, TransformError> {
self.expand(name, params, 0, 1)
}
}
impl Registry<TransformDef> for TransformRegistry {
fn contains(&self, name: &str) -> bool {
self.transforms.contains_key(name)
}
fn get(&self, name: &str) -> Option<&TransformDef> {
self.transforms.get(name)
}
fn len(&self) -> usize {
self.transforms.len()
}
fn clear(&mut self) {
self.transforms.clear();
}
fn names(&self) -> Box<dyn Iterator<Item = &String> + '_> {
Box::new(self.transforms.keys())
}
}
#[derive(Debug, Clone, Default)]
pub struct CompositionRegistry {
compositions: HashMap<String, Composition>,
}
impl CompositionRegistry {
pub fn new() -> Self {
Self { compositions: HashMap::new() }
}
pub fn register(&mut self, composition: Composition) {
self.compositions.insert(composition.name.clone(), composition);
}
pub fn get(&self, name: &str) -> Option<&Composition> {
self.compositions.get(name)
}
pub fn contains(&self, name: &str) -> bool {
self.compositions.contains_key(name)
}
pub fn len(&self) -> usize {
self.compositions.len()
}
pub fn is_empty(&self) -> bool {
self.compositions.is_empty()
}
pub fn clear(&mut self) {
self.compositions.clear();
}
pub fn names(&self) -> impl Iterator<Item = &String> {
self.compositions.keys()
}
pub fn iter(&self) -> impl Iterator<Item = (&String, &Composition)> {
self.compositions.iter()
}
}
impl Registry<Composition> for CompositionRegistry {
fn contains(&self, name: &str) -> bool {
self.compositions.contains_key(name)
}
fn get(&self, name: &str) -> Option<&Composition> {
self.compositions.get(name)
}
fn len(&self) -> usize {
self.compositions.len()
}
fn clear(&mut self) {
self.compositions.clear();
}
fn names(&self) -> Box<dyn Iterator<Item = &String> + '_> {
Box::new(self.compositions.keys())
}
}
#[derive(Debug, Clone)]
pub enum Renderable<'a> {
Sprite(&'a Sprite),
Composition(&'a Composition),
}
impl<'a> Renderable<'a> {
pub fn name(&self) -> &str {
match self {
Renderable::Sprite(sprite) => &sprite.name,
Renderable::Composition(composition) => &composition.name,
}
}
pub fn is_sprite(&self) -> bool {
matches!(self, Renderable::Sprite(_))
}
pub fn is_composition(&self) -> bool {
matches!(self, Renderable::Composition(_))
}
pub fn as_sprite(&self) -> Option<&'a Sprite> {
match self {
Renderable::Sprite(sprite) => Some(sprite),
_ => None,
}
}
pub fn as_composition(&self) -> Option<&'a Composition> {
match self {
Renderable::Composition(composition) => Some(composition),
_ => None,
}
}
}
pub fn lookup_renderable<'a>(
name: &str,
sprite_registry: &'a SpriteRegistry,
composition_registry: &'a CompositionRegistry,
) -> Option<Renderable<'a>> {
if let Some(sprite) = sprite_registry.get_sprite(name) {
return Some(Renderable::Sprite(sprite));
}
if let Some(composition) = composition_registry.get(name) {
return Some(Renderable::Composition(composition));
}
None
}
#[cfg(test)]
mod tests {
use super::*;
fn mono_palette() -> Palette {
Palette {
name: "mono".to_string(),
colors: HashMap::from([
("{_}".to_string(), "#00000000".to_string()),
("{on}".to_string(), "#FFFFFF".to_string()),
("{off}".to_string(), "#000000".to_string()),
]),
}
}
fn checker_sprite_named() -> Sprite {
Sprite {
name: "checker".to_string(),
size: None,
palette: PaletteRef::Named("mono".to_string()),
grid: vec!["{on}{off}{on}{off}".to_string(), "{off}{on}{off}{on}".to_string()],
metadata: None,
..Default::default()
}
}
fn dot_sprite_inline() -> Sprite {
Sprite {
name: "dot".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::from([
("{_}".to_string(), "#00000000".to_string()),
("{x}".to_string(), "#FF0000".to_string()),
])),
grid: vec!["{x}".to_string()],
metadata: None,
..Default::default()
}
}
fn bad_ref_sprite() -> Sprite {
Sprite {
name: "bad_ref".to_string(),
size: None,
palette: PaletteRef::Named("nonexistent".to_string()),
grid: vec!["{x}{x}".to_string()],
metadata: None,
..Default::default()
}
}
#[test]
fn test_registry_new_is_empty() {
let registry = PaletteRegistry::new();
assert!(!registry.contains("anything"));
}
#[test]
fn test_register_and_get() {
let mut registry = PaletteRegistry::new();
let palette = mono_palette();
registry.register(palette.clone());
assert!(registry.contains("mono"));
let retrieved = registry.get("mono").unwrap();
assert_eq!(retrieved.name, "mono");
assert_eq!(retrieved.colors.get("{on}"), Some(&"#FFFFFF".to_string()));
}
#[test]
fn test_register_overwrites() {
let mut registry = PaletteRegistry::new();
let palette1 = Palette {
name: "test".to_string(),
colors: HashMap::from([("{a}".to_string(), "#FF0000".to_string())]),
};
let palette2 = Palette {
name: "test".to_string(),
colors: HashMap::from([("{b}".to_string(), "#00FF00".to_string())]),
};
registry.register(palette1);
registry.register(palette2);
let retrieved = registry.get("test").unwrap();
assert!(retrieved.colors.contains_key("{b}"));
assert!(!retrieved.colors.contains_key("{a}"));
}
#[test]
fn test_resolve_strict_named_found() {
let mut registry = PaletteRegistry::new();
registry.register(mono_palette());
let sprite = checker_sprite_named();
let result = registry.resolve_strict(&sprite).unwrap();
assert_eq!(result.source, PaletteSource::Named("mono".to_string()));
assert_eq!(result.colors.get("{on}"), Some(&"#FFFFFF".to_string()));
}
#[test]
fn test_resolve_strict_named_not_found() {
let registry = PaletteRegistry::new();
let sprite = bad_ref_sprite();
let result = registry.resolve_strict(&sprite);
assert_eq!(result, Err(PaletteError::NotFound("nonexistent".to_string())));
}
#[test]
fn test_resolve_strict_inline() {
let registry = PaletteRegistry::new();
let sprite = dot_sprite_inline();
let result = registry.resolve_strict(&sprite).unwrap();
assert_eq!(result.source, PaletteSource::Inline);
assert_eq!(result.colors.get("{x}"), Some(&"#FF0000".to_string()));
}
#[test]
fn test_resolve_lenient_named_found() {
let mut registry = PaletteRegistry::new();
registry.register(mono_palette());
let sprite = checker_sprite_named();
let result = registry.resolve_lenient(&sprite);
assert!(result.warning.is_none());
assert_eq!(result.palette.source, PaletteSource::Named("mono".to_string()));
}
#[test]
fn test_resolve_lenient_named_not_found() {
let registry = PaletteRegistry::new();
let sprite = bad_ref_sprite();
let result = registry.resolve_lenient(&sprite);
assert!(result.warning.is_some());
assert!(result.warning.as_ref().unwrap().message.contains("nonexistent"));
assert_eq!(result.palette.source, PaletteSource::Fallback);
assert!(result.palette.colors.is_empty());
}
#[test]
fn test_resolve_lenient_inline() {
let registry = PaletteRegistry::new();
let sprite = dot_sprite_inline();
let result = registry.resolve_lenient(&sprite);
assert!(result.warning.is_none());
assert_eq!(result.palette.source, PaletteSource::Inline);
}
#[test]
fn test_resolve_combined_strict() {
let registry = PaletteRegistry::new();
let sprite = bad_ref_sprite();
let result = registry.resolve(&sprite, true);
assert!(result.is_err());
}
#[test]
fn test_resolve_combined_lenient() {
let registry = PaletteRegistry::new();
let sprite = bad_ref_sprite();
let result = registry.resolve(&sprite, false).unwrap();
assert!(result.warning.is_some());
assert_eq!(result.palette.source, PaletteSource::Fallback);
}
#[test]
fn test_fixture_named_palette() {
let mut registry = PaletteRegistry::new();
registry.register(mono_palette());
let sprite = checker_sprite_named();
let result = registry.resolve_strict(&sprite).unwrap();
assert_eq!(result.source, PaletteSource::Named("mono".to_string()));
assert_eq!(result.colors.len(), 3);
assert_eq!(result.colors.get("{_}"), Some(&"#00000000".to_string()));
assert_eq!(result.colors.get("{on}"), Some(&"#FFFFFF".to_string()));
assert_eq!(result.colors.get("{off}"), Some(&"#000000".to_string()));
}
#[test]
fn test_fixture_unknown_palette_ref_strict() {
let registry = PaletteRegistry::new();
let sprite = bad_ref_sprite();
let result = registry.resolve_strict(&sprite);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), PaletteError::NotFound("nonexistent".to_string()));
}
#[test]
fn test_fixture_unknown_palette_ref_lenient() {
let registry = PaletteRegistry::new();
let sprite = bad_ref_sprite();
let result = registry.resolve_lenient(&sprite);
assert!(result.warning.is_some());
assert_eq!(result.warning.unwrap().message, "Palette 'nonexistent' not found");
assert_eq!(result.palette.source, PaletteSource::Fallback);
}
fn builtin_gameboy_sprite() -> Sprite {
Sprite {
name: "test".to_string(),
size: None,
palette: PaletteRef::Named("@gameboy".to_string()),
grid: vec!["{lightest}{dark}".to_string()],
metadata: None,
..Default::default()
}
}
fn builtin_nonexistent_sprite() -> Sprite {
Sprite {
name: "test".to_string(),
size: None,
palette: PaletteRef::Named("@nonexistent".to_string()),
grid: vec!["{x}{x}".to_string()],
metadata: None,
..Default::default()
}
}
fn hero_sprite() -> Sprite {
Sprite {
name: "hero".to_string(),
size: Some([4, 4]),
palette: PaletteRef::Inline(HashMap::from([
("{_}".to_string(), "#00000000".to_string()),
("{skin}".to_string(), "#FFCC99".to_string()),
("{hair}".to_string(), "#333333".to_string()),
])),
grid: vec![
"{_}{hair}{hair}{_}".to_string(),
"{hair}{skin}{skin}{hair}".to_string(),
"{_}{skin}{skin}{_}".to_string(),
"{_}{skin}{skin}{_}".to_string(),
],
metadata: None,
..Default::default()
}
}
fn hero_red_variant() -> Variant {
Variant {
name: "hero_red".to_string(),
base: "hero".to_string(),
palette: HashMap::from([("{skin}".to_string(), "#FF6666".to_string())]),
..Default::default()
}
}
fn hero_alt_variant() -> Variant {
Variant {
name: "hero_alt".to_string(),
base: "hero".to_string(),
palette: HashMap::from([
("{skin}".to_string(), "#66FF66".to_string()),
("{hair}".to_string(), "#FFFF00".to_string()),
]),
..Default::default()
}
}
fn bad_base_variant() -> Variant {
Variant {
name: "ghost".to_string(),
base: "nonexistent".to_string(),
palette: HashMap::new(),
..Default::default()
}
}
#[test]
fn test_resolve_strict_builtin_found() {
let registry = PaletteRegistry::new();
let sprite = builtin_gameboy_sprite();
let result = registry.resolve_strict(&sprite).unwrap();
assert_eq!(result.source, PaletteSource::Builtin("gameboy".to_string()));
assert_eq!(result.colors.get("{lightest}"), Some(&"#9BBC0F".to_string()));
assert_eq!(result.colors.get("{dark}"), Some(&"#306230".to_string()));
}
#[test]
fn test_resolve_strict_builtin_not_found() {
let registry = PaletteRegistry::new();
let sprite = builtin_nonexistent_sprite();
let result = registry.resolve_strict(&sprite);
assert_eq!(result, Err(PaletteError::BuiltinNotFound("nonexistent".to_string())));
}
#[test]
fn test_resolve_lenient_builtin_found() {
let registry = PaletteRegistry::new();
let sprite = builtin_gameboy_sprite();
let result = registry.resolve_lenient(&sprite);
assert!(result.warning.is_none());
assert_eq!(result.palette.source, PaletteSource::Builtin("gameboy".to_string()));
assert_eq!(result.palette.colors.get("{lightest}"), Some(&"#9BBC0F".to_string()));
}
#[test]
fn test_resolve_lenient_builtin_not_found() {
let registry = PaletteRegistry::new();
let sprite = builtin_nonexistent_sprite();
let result = registry.resolve_lenient(&sprite);
assert!(result.warning.is_some());
assert_eq!(result.warning.unwrap().message, "Built-in palette 'nonexistent' not found");
assert_eq!(result.palette.source, PaletteSource::Fallback);
assert!(result.palette.colors.is_empty());
}
#[test]
fn test_resolve_combined_builtin_strict() {
let registry = PaletteRegistry::new();
let sprite = builtin_nonexistent_sprite();
let result = registry.resolve(&sprite, true);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), PaletteError::BuiltinNotFound("nonexistent".to_string()));
}
#[test]
fn test_resolve_combined_builtin_lenient() {
let registry = PaletteRegistry::new();
let sprite = builtin_nonexistent_sprite();
let result = registry.resolve(&sprite, false).unwrap();
assert!(result.warning.is_some());
assert_eq!(result.palette.source, PaletteSource::Fallback);
}
#[test]
fn test_fixture_builtin_palette() {
let registry = PaletteRegistry::new();
let sprite = builtin_gameboy_sprite();
let result = registry.resolve_strict(&sprite).unwrap();
assert_eq!(result.source, PaletteSource::Builtin("gameboy".to_string()));
assert_eq!(result.colors.get("{lightest}"), Some(&"#9BBC0F".to_string()));
assert_eq!(result.colors.get("{light}"), Some(&"#8BAC0F".to_string()));
assert_eq!(result.colors.get("{dark}"), Some(&"#306230".to_string()));
assert_eq!(result.colors.get("{darkest}"), Some(&"#0F380F".to_string()));
}
#[test]
fn test_all_builtins_resolvable() {
let registry = PaletteRegistry::new();
let builtin_names = ["gameboy", "nes", "pico8", "grayscale", "1bit"];
for name in builtin_names {
let sprite = Sprite {
name: "test".to_string(),
size: None,
palette: PaletteRef::Named(format!("@{}", name)),
grid: vec!["{_}".to_string()],
metadata: None,
..Default::default()
};
let result = registry.resolve_strict(&sprite);
assert!(result.is_ok(), "Built-in palette @{} should be resolvable", name);
assert_eq!(result.unwrap().source, PaletteSource::Builtin(name.to_string()));
}
}
#[test]
fn test_sprite_registry_new() {
let registry = SpriteRegistry::new();
assert!(!registry.contains("anything"));
}
#[test]
fn test_sprite_registry_register_sprite() {
let mut registry = SpriteRegistry::new();
registry.register_sprite(hero_sprite());
assert!(registry.contains("hero"));
assert!(registry.get_sprite("hero").is_some());
assert!(registry.get_variant("hero").is_none());
}
#[test]
fn test_sprite_registry_register_variant() {
let mut registry = SpriteRegistry::new();
registry.register_variant(hero_red_variant());
assert!(registry.contains("hero_red"));
assert!(registry.get_sprite("hero_red").is_none());
assert!(registry.get_variant("hero_red").is_some());
}
#[test]
fn test_sprite_registry_resolve_sprite() {
let mut sprite_registry = SpriteRegistry::new();
sprite_registry.register_sprite(hero_sprite());
let palette_registry = PaletteRegistry::new();
let result = sprite_registry.resolve("hero", &palette_registry, false).unwrap();
assert_eq!(result.name, "hero");
assert_eq!(result.size, Some([4, 4]));
assert_eq!(result.grid.len(), 4);
assert_eq!(result.palette.get("{skin}"), Some(&"#FFCC99".to_string()));
assert!(result.warnings.is_empty());
}
#[test]
fn test_sprite_registry_resolve_variant_single_override() {
let mut sprite_registry = SpriteRegistry::new();
sprite_registry.register_sprite(hero_sprite());
sprite_registry.register_variant(hero_red_variant());
let palette_registry = PaletteRegistry::new();
let result = sprite_registry.resolve("hero_red", &palette_registry, false).unwrap();
assert_eq!(result.name, "hero_red");
assert_eq!(result.size, Some([4, 4])); assert_eq!(result.grid.len(), 4);
assert_eq!(result.palette.get("{skin}"), Some(&"#FF6666".to_string()));
assert_eq!(result.palette.get("{hair}"), Some(&"#333333".to_string()));
assert_eq!(result.palette.get("{_}"), Some(&"#00000000".to_string()));
assert!(result.warnings.is_empty());
}
#[test]
fn test_sprite_registry_resolve_variant_multiple_overrides() {
let mut sprite_registry = SpriteRegistry::new();
sprite_registry.register_sprite(hero_sprite());
sprite_registry.register_variant(hero_alt_variant());
let palette_registry = PaletteRegistry::new();
let result = sprite_registry.resolve("hero_alt", &palette_registry, false).unwrap();
assert_eq!(result.name, "hero_alt");
assert_eq!(result.palette.get("{skin}"), Some(&"#66FF66".to_string()));
assert_eq!(result.palette.get("{hair}"), Some(&"#FFFF00".to_string()));
assert_eq!(result.palette.get("{_}"), Some(&"#00000000".to_string()));
}
#[test]
fn test_sprite_registry_variant_unknown_base_strict() {
let mut sprite_registry = SpriteRegistry::new();
sprite_registry.register_variant(bad_base_variant());
let palette_registry = PaletteRegistry::new();
let result = sprite_registry.resolve("ghost", &palette_registry, true);
assert!(result.is_err());
match result.unwrap_err() {
SpriteError::BaseNotFound { variant, base } => {
assert_eq!(variant, "ghost");
assert_eq!(base, "nonexistent");
}
_ => panic!("Expected BaseNotFound error"),
}
}
#[test]
fn test_sprite_registry_variant_unknown_base_lenient() {
let mut sprite_registry = SpriteRegistry::new();
sprite_registry.register_variant(bad_base_variant());
let palette_registry = PaletteRegistry::new();
let result = sprite_registry.resolve("ghost", &palette_registry, false).unwrap();
assert_eq!(result.name, "ghost");
assert!(result.grid.is_empty());
assert!(result.palette.is_empty());
assert_eq!(result.warnings.len(), 1);
assert!(result.warnings[0].message.contains("nonexistent"));
}
#[test]
fn test_sprite_registry_not_found_strict() {
let sprite_registry = SpriteRegistry::new();
let palette_registry = PaletteRegistry::new();
let result = sprite_registry.resolve("missing", &palette_registry, true);
assert!(result.is_err());
match result.unwrap_err() {
SpriteError::NotFound(name) => assert_eq!(name, "missing"),
_ => panic!("Expected NotFound error"),
}
}
#[test]
fn test_sprite_registry_not_found_lenient() {
let sprite_registry = SpriteRegistry::new();
let palette_registry = PaletteRegistry::new();
let result = sprite_registry.resolve("missing", &palette_registry, false).unwrap();
assert_eq!(result.name, "missing");
assert!(result.grid.is_empty());
assert_eq!(result.warnings.len(), 1);
}
#[test]
fn test_sprite_registry_variant_preserves_grid() {
let mut sprite_registry = SpriteRegistry::new();
sprite_registry.register_sprite(hero_sprite());
sprite_registry.register_variant(hero_red_variant());
let palette_registry = PaletteRegistry::new();
let sprite_result = sprite_registry.resolve("hero", &palette_registry, false).unwrap();
let variant_result = sprite_registry.resolve("hero_red", &palette_registry, false).unwrap();
assert_eq!(sprite_result.grid, variant_result.grid);
assert_eq!(sprite_result.size, variant_result.size);
}
#[test]
fn test_sprite_registry_variant_with_named_palette() {
let mut sprite_registry = SpriteRegistry::new();
let mut palette_registry = PaletteRegistry::new();
palette_registry.register(mono_palette());
let sprite = checker_sprite_named();
sprite_registry.register_sprite(sprite);
let variant = Variant {
name: "checker_red".to_string(),
base: "checker".to_string(),
palette: HashMap::from([("{on}".to_string(), "#FF0000".to_string())]),
..Default::default()
};
sprite_registry.register_variant(variant);
let result = sprite_registry.resolve("checker_red", &palette_registry, false).unwrap();
assert_eq!(result.name, "checker_red");
assert_eq!(result.palette.get("{on}"), Some(&"#FF0000".to_string()));
assert_eq!(result.palette.get("{off}"), Some(&"#000000".to_string()));
assert_eq!(result.palette.get("{_}"), Some(&"#00000000".to_string()));
}
#[test]
fn test_sprite_registry_names() {
let mut registry = SpriteRegistry::new();
registry.register_sprite(hero_sprite());
registry.register_variant(hero_red_variant());
let names: Vec<_> = registry.names().collect();
assert_eq!(names.len(), 2);
assert!(names.contains(&&"hero".to_string()));
assert!(names.contains(&&"hero_red".to_string()));
}
#[test]
fn test_resolve_sprite_with_source() {
let palette_registry = PaletteRegistry::new();
let mut sprite_registry = SpriteRegistry::new();
let base = Sprite {
name: "base".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::from([
("{_}".to_string(), "#00000000".to_string()),
("{x}".to_string(), "#FF0000".to_string()),
])),
grid: vec!["{x}{x}".to_string(), "{_}{x}".to_string()],
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(base);
let derived = Sprite {
name: "derived".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::from([
("{_}".to_string(), "#00000000".to_string()),
("{x}".to_string(), "#00FF00".to_string()), ])),
grid: vec![], source: Some("base".to_string()),
transform: None,
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(derived);
let result = sprite_registry.resolve("derived", &palette_registry, false).unwrap();
assert_eq!(result.name, "derived");
assert_eq!(result.grid.len(), 2);
assert_eq!(result.grid[0], "{x}{x}");
assert_eq!(result.grid[1], "{_}{x}");
}
#[test]
fn test_resolve_sprite_with_mirror_h_transform() {
let palette_registry = PaletteRegistry::new();
let mut sprite_registry = SpriteRegistry::new();
let base = Sprite {
name: "base".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::from([
("{_}".to_string(), "#00000000".to_string()),
("{a}".to_string(), "#FF0000".to_string()),
("{b}".to_string(), "#00FF00".to_string()),
])),
grid: vec!["{a}{b}".to_string()],
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(base);
let mirrored = Sprite {
name: "mirrored".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::from([
("{_}".to_string(), "#00000000".to_string()),
("{a}".to_string(), "#FF0000".to_string()),
("{b}".to_string(), "#00FF00".to_string()),
])),
grid: vec![],
source: Some("base".to_string()),
transform: Some(vec![TransformSpec::String("mirror-h".to_string())]),
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(mirrored);
let result = sprite_registry.resolve("mirrored", &palette_registry, false).unwrap();
assert_eq!(result.grid.len(), 1);
assert_eq!(result.grid[0], "{b}{a}");
}
#[test]
fn test_resolve_sprite_with_rotate_transform() {
let palette_registry = PaletteRegistry::new();
let mut sprite_registry = SpriteRegistry::new();
let base = Sprite {
name: "base".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::from([
("{1}".to_string(), "#FF0000".to_string()),
("{2}".to_string(), "#00FF00".to_string()),
("{3}".to_string(), "#0000FF".to_string()),
("{4}".to_string(), "#FFFF00".to_string()),
])),
grid: vec!["{1}{2}".to_string(), "{3}{4}".to_string()],
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(base);
let rotated = Sprite {
name: "rotated".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::new()),
grid: vec![],
source: Some("base".to_string()),
transform: Some(vec![TransformSpec::String("rotate:90".to_string())]),
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(rotated);
let result = sprite_registry.resolve("rotated", &palette_registry, false).unwrap();
assert_eq!(result.grid.len(), 2);
assert_eq!(result.grid[0], "{3}{1}");
assert_eq!(result.grid[1], "{4}{2}");
}
#[test]
fn test_resolve_sprite_with_chained_transforms() {
let palette_registry = PaletteRegistry::new();
let mut sprite_registry = SpriteRegistry::new();
let base = Sprite {
name: "base".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::from([
("{a}".to_string(), "#FF0000".to_string()),
("{b}".to_string(), "#00FF00".to_string()),
])),
grid: vec!["{a}{b}".to_string()],
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(base);
let transformed = Sprite {
name: "transformed".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::new()),
grid: vec![],
source: Some("base".to_string()),
transform: Some(vec![
TransformSpec::String("mirror-h".to_string()),
TransformSpec::String("tile:2x1".to_string()),
]),
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(transformed);
let result = sprite_registry.resolve("transformed", &palette_registry, false).unwrap();
assert_eq!(result.grid.len(), 1);
assert_eq!(result.grid[0], "{b}{a}{b}{a}");
}
#[test]
fn test_resolve_sprite_source_not_found_strict() {
let palette_registry = PaletteRegistry::new();
let mut sprite_registry = SpriteRegistry::new();
let derived = Sprite {
name: "derived".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::new()),
grid: vec![],
source: Some("nonexistent".to_string()),
transform: None,
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(derived);
let result = sprite_registry.resolve("derived", &palette_registry, true);
assert!(result.is_err());
match result.unwrap_err() {
SpriteError::SourceNotFound { sprite, source_name } => {
assert_eq!(sprite, "derived");
assert_eq!(source_name, "nonexistent");
}
e => panic!("Expected SourceNotFound, got {:?}", e),
}
}
#[test]
fn test_resolve_sprite_source_not_found_lenient() {
let palette_registry = PaletteRegistry::new();
let mut sprite_registry = SpriteRegistry::new();
let derived = Sprite {
name: "derived".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::new()),
grid: vec![],
source: Some("nonexistent".to_string()),
transform: None,
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(derived);
let result = sprite_registry.resolve("derived", &palette_registry, false).unwrap();
assert!(result.grid.is_empty());
assert!(!result.warnings.is_empty());
}
#[test]
fn test_resolve_sprite_circular_reference_strict() {
let palette_registry = PaletteRegistry::new();
let mut sprite_registry = SpriteRegistry::new();
let a = Sprite {
name: "a".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::new()),
grid: vec![],
source: Some("b".to_string()),
transform: None,
metadata: None,
..Default::default()
};
let b = Sprite {
name: "b".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::new()),
grid: vec![],
source: Some("a".to_string()),
transform: None,
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(a);
sprite_registry.register_sprite(b);
let result = sprite_registry.resolve("a", &palette_registry, true);
assert!(result.is_err());
match result.unwrap_err() {
SpriteError::CircularReference { sprite, chain } => {
assert_eq!(sprite, "a");
assert!(chain.len() >= 2);
}
e => panic!("Expected CircularReference, got {:?}", e),
}
}
#[test]
fn test_resolve_variant_with_transform() {
let palette_registry = PaletteRegistry::new();
let mut sprite_registry = SpriteRegistry::new();
let base = Sprite {
name: "base".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::from([
("{a}".to_string(), "#FF0000".to_string()),
("{b}".to_string(), "#00FF00".to_string()),
])),
grid: vec!["{a}{b}".to_string()],
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(base);
let variant = Variant {
name: "variant".to_string(),
base: "base".to_string(),
palette: HashMap::from([("{a}".to_string(), "#0000FF".to_string())]),
transform: Some(vec![TransformSpec::String("mirror-h".to_string())]),
};
sprite_registry.register_variant(variant);
let result = sprite_registry.resolve("variant", &palette_registry, false).unwrap();
assert_eq!(result.grid[0], "{b}{a}");
assert_eq!(result.palette.get("{a}").unwrap(), "#0000FF");
assert_eq!(result.palette.get("{b}").unwrap(), "#00FF00");
}
#[test]
fn test_palette_registry_trait_len_and_empty() {
let mut registry = PaletteRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
registry.register(mono_palette());
assert!(!registry.is_empty());
assert_eq!(registry.len(), 1);
registry.register(Palette { name: "other".to_string(), colors: HashMap::new() });
assert_eq!(registry.len(), 2);
registry.clear();
assert!(registry.is_empty());
}
#[test]
fn test_palette_registry_trait_names() {
let mut registry = PaletteRegistry::new();
registry.register(mono_palette());
registry.register(Palette { name: "other".to_string(), colors: HashMap::new() });
let names: Vec<_> = registry.names().collect();
assert_eq!(names.len(), 2);
assert!(names.contains(&&"mono".to_string()));
assert!(names.contains(&&"other".to_string()));
}
#[test]
fn test_palette_registry_trait_via_generic() {
fn check_registry<V>(reg: &impl Registry<V>) -> usize {
reg.len()
}
let mut registry = PaletteRegistry::new();
registry.register(mono_palette());
assert_eq!(check_registry::<Palette>(®istry), 1);
}
#[test]
fn test_sprite_registry_len_and_empty() {
let mut registry = SpriteRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
registry.register_sprite(hero_sprite());
assert!(!registry.is_empty());
assert_eq!(registry.len(), 1);
assert_eq!(registry.sprite_count(), 1);
assert_eq!(registry.variant_count(), 0);
registry.register_variant(hero_red_variant());
assert_eq!(registry.len(), 2);
assert_eq!(registry.sprite_count(), 1);
assert_eq!(registry.variant_count(), 1);
registry.clear();
assert!(registry.is_empty());
}
#[test]
fn test_sprite_registry_sprite_trait_via_generic() {
fn check_registry<V>(reg: &impl Registry<V>) -> usize {
reg.len()
}
let mut registry = SpriteRegistry::new();
registry.register_sprite(hero_sprite());
registry.register_variant(hero_red_variant());
assert_eq!(check_registry::<Sprite>(®istry), 1);
assert_eq!(check_registry::<Variant>(®istry), 1);
}
#[test]
fn test_sprite_registry_trait_contains() {
let mut registry = SpriteRegistry::new();
registry.register_sprite(hero_sprite());
registry.register_variant(hero_red_variant());
assert!(Registry::<Sprite>::contains(®istry, "hero"));
assert!(!Registry::<Sprite>::contains(®istry, "hero_red"));
assert!(!Registry::<Variant>::contains(®istry, "hero"));
assert!(Registry::<Variant>::contains(®istry, "hero_red"));
assert!(registry.contains("hero"));
assert!(registry.contains("hero_red"));
}
#[test]
fn test_sprite_registry_trait_get() {
let mut registry = SpriteRegistry::new();
registry.register_sprite(hero_sprite());
registry.register_variant(hero_red_variant());
let sprite = Registry::<Sprite>::get(®istry, "hero");
assert!(sprite.is_some());
assert_eq!(sprite.unwrap().name, "hero");
let variant = Registry::<Variant>::get(®istry, "hero_red");
assert!(variant.is_some());
assert_eq!(variant.unwrap().name, "hero_red");
}
fn test_composition() -> Composition {
Composition {
name: "hero_scene".to_string(),
base: None,
size: Some([16, 16]),
cell_size: Some([8, 8]),
sprites: HashMap::from([
("hero".to_string(), Some("hero".to_string())),
("bg".to_string(), Some("background".to_string())),
]),
layers: vec![],
}
}
fn alt_composition() -> Composition {
Composition {
name: "alt_scene".to_string(),
base: None,
size: Some([32, 32]),
cell_size: None,
sprites: HashMap::new(),
layers: vec![],
}
}
#[test]
fn test_composition_registry_new() {
let registry = CompositionRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
assert!(!registry.contains("anything"));
}
#[test]
fn test_composition_registry_register_and_get() {
let mut registry = CompositionRegistry::new();
let composition = test_composition();
registry.register(composition);
assert!(registry.contains("hero_scene"));
let retrieved = registry.get("hero_scene").unwrap();
assert_eq!(retrieved.name, "hero_scene");
assert_eq!(retrieved.size, Some([16, 16]));
}
#[test]
fn test_composition_registry_register_overwrites() {
let mut registry = CompositionRegistry::new();
let comp1 = Composition {
name: "scene".to_string(),
base: None,
size: Some([8, 8]),
cell_size: None,
sprites: HashMap::new(),
layers: vec![],
};
let comp2 = Composition {
name: "scene".to_string(),
base: None,
size: Some([16, 16]),
cell_size: None,
sprites: HashMap::new(),
layers: vec![],
};
registry.register(comp1);
registry.register(comp2);
assert_eq!(registry.len(), 1);
let retrieved = registry.get("scene").unwrap();
assert_eq!(retrieved.size, Some([16, 16]));
}
#[test]
fn test_composition_registry_len_and_empty() {
let mut registry = CompositionRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
registry.register(test_composition());
assert!(!registry.is_empty());
assert_eq!(registry.len(), 1);
registry.register(alt_composition());
assert_eq!(registry.len(), 2);
registry.clear();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
}
#[test]
fn test_composition_registry_names() {
let mut registry = CompositionRegistry::new();
registry.register(test_composition());
registry.register(alt_composition());
let names: Vec<_> = registry.names().collect();
assert_eq!(names.len(), 2);
assert!(names.contains(&&"hero_scene".to_string()));
assert!(names.contains(&&"alt_scene".to_string()));
}
#[test]
fn test_composition_registry_iter() {
let mut registry = CompositionRegistry::new();
registry.register(test_composition());
registry.register(alt_composition());
let items: Vec<_> = registry.iter().collect();
assert_eq!(items.len(), 2);
}
#[test]
fn test_composition_registry_trait_via_generic() {
fn check_registry<V>(reg: &impl Registry<V>) -> usize {
reg.len()
}
let mut registry = CompositionRegistry::new();
registry.register(test_composition());
assert_eq!(check_registry::<Composition>(®istry), 1);
}
#[test]
fn test_composition_registry_trait_contains_and_get() {
let mut registry = CompositionRegistry::new();
registry.register(test_composition());
assert!(Registry::<Composition>::contains(®istry, "hero_scene"));
assert!(!Registry::<Composition>::contains(®istry, "nonexistent"));
let composition = Registry::<Composition>::get(®istry, "hero_scene");
assert!(composition.is_some());
assert_eq!(composition.unwrap().name, "hero_scene");
}
#[test]
fn test_renderable_sprite() {
let sprite = hero_sprite();
let renderable = Renderable::Sprite(&sprite);
assert_eq!(renderable.name(), "hero");
assert!(renderable.is_sprite());
assert!(!renderable.is_composition());
assert!(renderable.as_sprite().is_some());
assert!(renderable.as_composition().is_none());
}
#[test]
fn test_renderable_composition() {
let composition = test_composition();
let renderable = Renderable::Composition(&composition);
assert_eq!(renderable.name(), "hero_scene");
assert!(!renderable.is_sprite());
assert!(renderable.is_composition());
assert!(renderable.as_sprite().is_none());
assert!(renderable.as_composition().is_some());
}
#[test]
fn test_lookup_renderable_finds_sprite() {
let mut sprite_registry = SpriteRegistry::new();
sprite_registry.register_sprite(hero_sprite());
let composition_registry = CompositionRegistry::new();
let result = lookup_renderable("hero", &sprite_registry, &composition_registry);
assert!(result.is_some());
let renderable = result.unwrap();
assert!(renderable.is_sprite());
assert_eq!(renderable.name(), "hero");
}
#[test]
fn test_lookup_renderable_finds_composition() {
let sprite_registry = SpriteRegistry::new();
let mut composition_registry = CompositionRegistry::new();
composition_registry.register(test_composition());
let result = lookup_renderable("hero_scene", &sprite_registry, &composition_registry);
assert!(result.is_some());
let renderable = result.unwrap();
assert!(renderable.is_composition());
assert_eq!(renderable.name(), "hero_scene");
}
#[test]
fn test_lookup_renderable_sprite_takes_precedence() {
let mut sprite_registry = SpriteRegistry::new();
let sprite = Sprite {
name: "shared_name".to_string(),
size: None,
palette: PaletteRef::Inline(HashMap::new()),
grid: vec![],
metadata: None,
..Default::default()
};
sprite_registry.register_sprite(sprite);
let mut composition_registry = CompositionRegistry::new();
let composition = Composition {
name: "shared_name".to_string(),
base: None,
size: None,
cell_size: None,
sprites: HashMap::new(),
layers: vec![],
};
composition_registry.register(composition);
let result = lookup_renderable("shared_name", &sprite_registry, &composition_registry);
assert!(result.is_some());
assert!(result.unwrap().is_sprite());
}
#[test]
fn test_lookup_renderable_not_found() {
let sprite_registry = SpriteRegistry::new();
let composition_registry = CompositionRegistry::new();
let result = lookup_renderable("nonexistent", &sprite_registry, &composition_registry);
assert!(result.is_none());
}
}