use std::collections::BTreeSet;
use serde::Serialize;
use serde_json::Value;
use crate::pass::{Given, OptionSpec, Options, Pass, PassLog, PassSpec};
use crate::{Component, Entity, JsonError, Name, Source, Texture, TextureFile, World};
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct TextureForms {
pub detail: u8,
pub forms: Vec<TextureForm>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
pub struct TextureForm {
pub detail: u8,
pub texture: Entity,
}
impl TextureForms {
pub fn choose(&self, choice: TextureChoice) -> usize {
match choice {
TextureChoice::Finest => 0,
TextureChoice::AtMost(n) => std::iter::once(self.detail)
.chain(self.forms.iter().map(|f| f.detail))
.enumerate()
.filter(|&(_, d)| d <= n)
.fold(None, |best: Option<(usize, u8)>, (i, d)| match best {
Some((_, b)) if b >= d => best,
_ => Some((i, d)),
})
.map_or(0, |(i, _)| i),
}
}
}
fn has_texture(w: &World, e: Entity) -> bool {
w.has::<Texture>(e) || w.has::<TextureFile>(e)
}
impl Component for TextureForms {
const NAME: &'static str = "core.texture_forms";
const GEOMETRY: crate::Geometry = crate::Geometry::None;
fn to_json(&self) -> Result<Option<Value>, JsonError> {
crate::json::to_value(self).map(Some)
}
fn references(&self, out: &mut Vec<Entity>) {
out.extend(self.forms.iter().map(|f| f.texture));
}
fn verify(&self, e: Entity, w: &World) -> Result<(), String> {
if !has_texture(w, e) {
return Err("other forms of an entity without a texture".into());
}
let mut seen = BTreeSet::new();
for f in &self.forms {
if f.texture == e || !w.is_alive(f.texture) {
return Err(format!("form {:?} is not another alive entity", f.texture));
}
if !seen.insert(f.texture) {
return Err(format!("form {:?} is listed twice", f.texture));
}
if !has_texture(w, f.texture) {
return Err(format!("form {:?} holds no texture", f.texture));
}
if w.has::<TextureForms>(f.texture) {
return Err(format!("form {:?} has forms of its own", f.texture));
}
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextureChoice {
Finest,
AtMost(u8),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct SelectTextureForm {
pub choice: TextureChoice,
}
pub const SELECT_TEXTURE_FORM: PassSpec = PassSpec {
name: "select-texture-form",
options: &[OptionSpec::required(
"form",
"finest",
"finest or a level 0-255: the stored form each texture keeps (TextureChoice)",
)],
make: select_texture_form,
};
fn select_texture_form(g: &Given) -> Result<Box<dyn Pass>, String> {
let choice = match g.required("form") {
"finest" => TextureChoice::Finest,
n => TextureChoice::AtMost(
n.parse()
.map_err(|_| format!("form={n} is not finest or a level 0-255"))?,
),
};
Ok(Box::new(SelectTextureForm { choice }))
}
impl Pass for SelectTextureForm {
fn name(&self) -> &'static str {
"select-texture-form"
}
fn options(&self) -> Options {
let form = match self.choice {
TextureChoice::Finest => "finest".to_owned(),
TextureChoice::AtMost(n) => n.to_string(),
};
Options::from([("form".to_owned(), form)])
}
fn removes(&self) -> &[&'static str] {
&[TextureForms::NAME, Texture::NAME, TextureFile::NAME]
}
fn modifies(&self) -> &[&'static str] {
&[Texture::NAME, TextureFile::NAME]
}
fn despawns_entities(&self) -> bool {
true
}
fn run(&self, w: &mut World, log: &mut PassLog) -> Result<(), String> {
let all: Vec<(Entity, TextureForms)> =
w.iter::<TextureForms>().map(|(e, f)| (e, f.clone())).collect();
let mut forms = BTreeSet::new();
for (texture, held) in &all {
held.verify(*texture, w)
.map_err(|m| format!("{texture:?}: {m}"))?;
for f in &held.forms {
if !forms.insert(f.texture) {
return Err(format!("form {:?} belongs to more than one texture", f.texture));
}
}
}
if let Some((name, holder, form)) = w
.references_to(&forms)
.into_iter()
.find(|(name, _, _)| *name != TextureForms::NAME)
{
return Err(format!("{name} on {holder:?} refers to form {form:?}"));
}
for (texture, held) in all {
let chosen = held.choose(self.choice);
match chosen.checked_sub(1).map(|k| held.forms[k]) {
None => log.count("textures kept in their own form", 1),
Some(f) => {
for (gone, name) in [
(w.remove::<Texture>(texture).is_some(), Texture::NAME),
(w.remove::<TextureFile>(texture).is_some(), TextureFile::NAME),
] {
if gone {
log.record_removed(name);
}
}
for (left, name) in [
(w.remove::<Name>(f.texture).is_some(), Name::NAME),
(w.remove::<Source>(f.texture).is_some(), Source::NAME),
] {
if left {
log.record_removed(name);
log.count(
&format!("{name} of the kept form left behind: the texture keeps its own"),
1,
);
}
}
let moved = w.move_components(f.texture, texture)?;
debug_assert!(moved, "checked alive above");
log.count("textures kept in another form", 1);
}
}
for (k, f) in held.forms.iter().enumerate() {
if k + 1 != chosen {
crate::ledger::despawn(w, f.texture, log, "texture forms left out");
log.count("texture forms left out", 1);
}
}
w.remove::<TextureForms>(texture);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pass::PassRegistry;
use crate::pbr::{PbrMaterial, TextureRef};
use crate::{ColorSpace, TexFormat, TexRole};
fn texture(side: u32, value: u8) -> Texture {
Texture {
width: side,
height: side,
format: TexFormat::Rgba8Unorm,
levels: vec![vec![value; (side * side * 4) as usize]],
color_space: ColorSpace::Srgb,
role: TexRole::Color,
normal: None,
}
}
fn world() -> (World, Entity, Entity, Entity) {
let mut w = World::new();
let own = w.spawn();
w.insert(own, texture(8, 200));
w.insert(own, Name("aet000_001_a".into()));
let low = w.spawn();
w.insert(low, texture(2, 50));
w.insert(low, Name("aet000_001_a_l".into()));
w.insert(
own,
TextureForms {
detail: 0,
forms: vec![TextureForm {
detail: 1,
texture: low,
}],
},
);
let material = w.spawn();
w.insert(
material,
PbrMaterial {
base_color_texture: Some(TextureRef::new(own)),
..PbrMaterial::default()
},
);
(w, own, low, material)
}
fn run(choice: TextureChoice, w: &mut World) -> PassLog {
let mut log = PassLog::default();
SelectTextureForm { choice }.run(w, &mut log).unwrap();
log
}
#[test]
fn a_texture_keeps_the_form_chosen_and_its_bindings() {
let (mut w, own, low, material) = world();
let log = run(TextureChoice::Finest, &mut w);
assert_eq!(log.counts["textures kept in their own form"], 1);
assert_eq!(w.get::<Texture>(own).unwrap().width, 8);
assert!(!w.is_alive(low) && !w.has::<TextureForms>(own));
let (mut w, own, low, material_2) = world();
let log = run(TextureChoice::AtMost(1), &mut w);
assert_eq!(log.counts["textures kept in another form"], 1);
let kept = w.get::<Texture>(own).unwrap();
assert_eq!((kept.width, kept.levels[0][0]), (2, 50));
assert_eq!(w.get::<Name>(own).unwrap().0, "aet000_001_a");
assert!(!w.is_alive(low) && !w.has::<TextureForms>(own));
let bound = w
.get::<PbrMaterial>(material_2)
.unwrap()
.base_color_texture
.as_ref()
.map(|t| t.texture);
assert_eq!(bound, Some(own));
let _ = material;
let (mut w, own, _, _) = world();
run(TextureChoice::AtMost(0), &mut w);
assert_eq!(w.get::<Texture>(own).unwrap().width, 8);
}
#[test]
fn an_invalid_selection_changes_nothing() {
let (mut w, own, low, material) = world();
w.get_mut::<PbrMaterial>(material).unwrap().base_color_texture = Some(TextureRef::new(low));
let err = SelectTextureForm {
choice: TextureChoice::Finest,
}
.run(&mut w, &mut PassLog::default())
.unwrap_err();
assert!(err.contains("refers to form"), "{err}");
assert!(w.is_alive(low) && w.has::<TextureForms>(own));
let (mut w, own, low, _) = world();
w.remove::<Texture>(low);
assert_eq!(
w.get::<TextureForms>(own).unwrap().verify(own, &w),
Err(format!("form {low:?} holds no texture"))
);
let (mut w, _, low, _) = world();
let other = w.spawn();
w.insert(other, texture(4, 1));
w.insert(
other,
TextureForms {
detail: 0,
forms: vec![TextureForm {
detail: 1,
texture: low,
}],
},
);
let err = SelectTextureForm {
choice: TextureChoice::Finest,
}
.run(&mut w, &mut PassLog::default())
.unwrap_err();
assert!(err.contains("belongs to more than one texture"), "{err}");
}
#[test]
fn the_pass_states_its_options() {
let mut reg = PassRegistry::new();
reg.add(&SELECT_TEXTURE_FORM);
for form in ["finest", "1"] {
let options = Options::from([("form".to_owned(), form.to_owned())]);
assert_eq!(
reg.make("select-texture-form", &options).unwrap().options(),
options
);
}
assert!(reg.make("select-texture-form", &Options::new()).is_err());
}
}