use anyhow::{bail, Context, Result}; use cgmath::{Deg, Point3}; use std::collections::{HashMap, HashSet}; use crate::physics::Polar; /// Toml file syntax pub(in crate::content) mod toml { use serde::Deserialize; use std::collections::HashMap; #[derive(Debug, Deserialize)] pub struct SystemRoot { pub system: System, pub object: HashMap, } #[derive(Debug, Deserialize)] pub struct System { pub name: String, } #[derive(Debug, Deserialize)] pub struct Object { pub sprite: String, pub position: Position, pub size: f32, pub radius: Option, pub angle: Option, } #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum Position { Polar(PolarCoords), Cartesian(CoordinatesThree), } #[derive(Debug, Deserialize)] pub struct PolarCoords { pub center: CoordinatesTwo, pub radius: f32, pub angle: f32, pub z: f32, } #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum CoordinatesTwo { Label(String), Coords([f32; 2]), } impl ToString for CoordinatesTwo { fn to_string(&self) -> String { match self { Self::Label(s) => s.to_owned(), Self::Coords(v) => format!("{:?}", v), } } } #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum CoordinatesThree { Label(String), Coords([f32; 3]), } impl ToString for CoordinatesThree { fn to_string(&self) -> String { match self { Self::Label(s) => s.to_owned(), Self::Coords(v) => format!("{:?}", v), } } } } #[derive(Debug)] pub struct System { pub name: String, pub objects: Vec, } #[derive(Debug)] pub struct Object { pub sprite: String, pub position: Point3, pub size: f32, pub angle: Deg, } // Helper function for resolve_position, never called on its own. fn resolve_coordinates( objects: &HashMap, cor: &toml::CoordinatesThree, mut cycle_detector: HashSet, ) -> Result> { match cor { toml::CoordinatesThree::Coords(c) => Ok((*c).into()), toml::CoordinatesThree::Label(l) => { if cycle_detector.contains(l) { bail!( "Found coordinate cycle: `{}`", cycle_detector.iter().fold(String::new(), |sum, a| { if sum.is_empty() { a.to_string() } else { sum + " -> " + a } }) ); } cycle_detector.insert(l.to_owned()); let p = match objects.get(l) { Some(p) => p, None => bail!("Could not resolve coordinate label `{l}`"), }; Ok(resolve_position(&objects, &p, cycle_detector) .with_context(|| format!("In object {:#?}", l))?) } } } /// Given an object, resolve it's position as a Point3. fn resolve_position( objects: &HashMap, obj: &toml::Object, cycle_detector: HashSet, ) -> Result> { match &obj.position { toml::Position::Cartesian(c) => Ok(resolve_coordinates(objects, &c, cycle_detector)?), toml::Position::Polar(p) => { let three = match &p.center { toml::CoordinatesTwo::Label(s) => toml::CoordinatesThree::Label(s.clone()), toml::CoordinatesTwo::Coords(v) => { toml::CoordinatesThree::Coords([v[0], v[1], f32::NAN]) } }; let r = resolve_coordinates(&objects, &three, cycle_detector)?; let plane = Polar { center: (r.x, r.y).into(), radius: p.radius, angle: Deg(p.angle), } .to_cartesian(); Ok(Point3 { x: plane.x, y: plane.y, z: p.z, }) } } } impl System { pub fn parse(value: toml::SystemRoot) -> Result { let mut objects = Vec::new(); for (label, obj) in &value.object { let mut cycle_detector = HashSet::new(); cycle_detector.insert(label.to_owned()); objects.push(Object { sprite: obj.sprite.clone(), position: resolve_position(&value.object, &obj, cycle_detector) .with_context(|| format!("In object {:#?}", label))?, size: obj.size, angle: Deg(obj.angle.unwrap_or(0.0)), }); } return Ok(Self { name: value.system.name.clone(), objects, }); } }