use anyhow::{bail, Context, Result}; use cgmath::{Deg, Point3}; use std::collections::{HashMap, HashSet}; use crate::physics::Polar; pub(super) mod syntax { use super::HashMap; use serde::Deserialize; // Raw serde syntax structs. // These are never seen by code outside this crate. #[derive(Debug, Deserialize)] pub struct System { pub object: HashMap, } #[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), } } } } // Processed data structs. // These are exported. #[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: &syntax::CoordinatesThree, mut cycle_detector: HashSet, ) -> Result> { match cor { syntax::CoordinatesThree::Coords(c) => Ok((*c).into()), syntax::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 its position as a Point3. fn resolve_position( objects: &HashMap, obj: &syntax::Object, cycle_detector: HashSet, ) -> Result> { match &obj.position { syntax::Position::Cartesian(c) => Ok(resolve_coordinates(objects, &c, cycle_detector)?), syntax::Position::Polar(p) => { let three = match &p.center { syntax::CoordinatesTwo::Label(s) => syntax::CoordinatesThree::Label(s.clone()), syntax::CoordinatesTwo::Coords(v) => { syntax::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 super::Build for System { fn build(root: &super::syntax::Root) -> Result> { let mut out = Vec::new(); for (system_name, system) in &root.system { let mut objects = Vec::new(); for (label, obj) in &system.object { let mut cycle_detector = HashSet::new(); cycle_detector.insert(label.to_owned()); objects.push(Object { sprite: obj.sprite.clone(), position: resolve_position(&system.object, &obj, cycle_detector) .with_context(|| format!("In object {:#?}", label))?, size: obj.size, angle: Deg(obj.angle.unwrap_or(0.0)), }); } out.push(Self { name: system_name.to_owned(), objects, }); } return Ok(out); } }