1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
use std::f32::consts::TAU;

use bevy::prelude::Transform;
use de_types::projection::ToAltitude;
use glam::{Quat, Vec2};
use serde::{Deserialize, Serialize};
use thiserror::Error;

use crate::{hash::MapHasher, size::MapBounds};

#[derive(Clone, Copy, Serialize, Deserialize)]
pub struct Placement {
    position: Vec2,
    heading: f32,
}

impl Placement {
    pub(crate) fn new(position: Vec2, heading: f32) -> Self {
        Self { position, heading }
    }

    pub(crate) fn update_hash(&self, hasher: &mut MapHasher) {
        hasher.update_vec2(self.position);
        hasher.update_f32(self.heading);
    }

    pub fn position(&self) -> Vec2 {
        self.position
    }

    /// Produces world to object transform which can be used to position the
    /// object on the map.
    pub fn to_transform(self) -> Transform {
        let rotation = Quat::from_rotation_y(self.heading);
        Transform {
            translation: self.position.to_msl(),
            rotation,
            ..Default::default()
        }
    }

    pub(crate) fn validate(&self, map_bounds: MapBounds) -> Result<(), PlacementValidationError> {
        if !map_bounds.contains(self.position) {
            return Err(PlacementValidationError::OutOfMapBound(self.position));
        }
        if !self.heading.is_finite() || self.heading < 0. || self.heading >= TAU {
            return Err(PlacementValidationError::InvalidHeading(self.heading));
        }
        Ok(())
    }
}

#[derive(Error, Debug)]
pub enum PlacementValidationError {
    #[error("position ({}, {}) is out of map bounds", .0.x, .0.y)]
    OutOfMapBound(Vec2),
    #[error("heading must be between 0 and τ (2π), got: {0}")]
    InvalidHeading(f32),
}