use super::*;
use crate::{
asset::{Resource, ResourceData},
core::{
algebra::{Matrix4, Point3, Vector2},
reflect::prelude::*,
type_traits::prelude::*,
visitor::prelude::*,
},
};
use fyrox_core::algebra::Point2;
use std::{
error::Error,
fmt::{Debug, Display, Formatter},
num::{ParseFloatError, ParseIntError},
path::Path,
str::FromStr,
};
use strum_macros::{AsRefStr, EnumString, VariantNames};
#[derive(
Clone,
Hash,
PartialEq,
Eq,
Default,
Visit,
Reflect,
AsRefStr,
EnumString,
VariantNames,
TypeUuidProvider,
)]
#[type_uuid(id = "04a44fec-394f-4497-97d5-fe9e6f915831")]
pub enum TileCollider {
#[default]
None,
Rectangle,
Custom(CustomTileColliderResource),
Mesh,
}
impl Default for &TileCollider {
fn default() -> Self {
&TileCollider::None
}
}
impl Debug for TileCollider {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::None => write!(f, "None"),
Self::Rectangle => write!(f, "Rectangle"),
Self::Custom(r) => {
if r.is_ok() {
write!(f, "Custom({})", r.data_ref().deref())
} else {
f.write_str("Custom(unloaded)")
}
}
Self::Mesh => write!(f, "Mesh"),
}
}
}
impl OrthoTransform for TileCollider {
fn x_flipped(self) -> Self {
if let Self::Custom(collider) = self {
let collider = collider.data_ref().clone();
Self::Custom(Resource::new_ok(
Uuid::new_v4(),
ResourceKind::Embedded,
collider.x_flipped(),
))
} else {
self
}
}
fn rotated(self, amount: i8) -> Self {
if let Self::Custom(collider) = self {
let collider = collider.data_ref().clone();
Self::Custom(Resource::new_ok(
Uuid::new_v4(),
ResourceKind::Embedded,
collider.rotated(amount),
))
} else {
self
}
}
}
impl TileCollider {
pub fn is_none(&self) -> bool {
matches!(self, TileCollider::None)
}
pub fn is_rectangle(&self) -> bool {
matches!(self, TileCollider::Rectangle)
}
pub fn is_custom(&self) -> bool {
matches!(self, TileCollider::Custom(_))
}
pub fn build_collider_shape(
&self,
transform: &Matrix4<f32>,
position: Vector3<f32>,
vertices: &mut Vec<Point2<f32>>,
triangles: &mut Vec<[u32; 3]>,
) {
match self {
TileCollider::None => (),
TileCollider::Rectangle => {
let origin = vertices.len() as u32;
for (dx, dy) in [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] {
let offset = Vector3::new(dx, dy, 0.0);
let point = Point3::from(position + offset);
vertices.push(transform.transform_point(&point).xy());
}
triangles.push([origin, origin + 1, origin + 2]);
triangles.push([origin, origin + 2, origin + 3]);
}
TileCollider::Custom(resource) => {
if resource.is_ok() {
resource
.data_ref()
.build_collider_shape(transform, position, vertices, triangles);
}
}
TileCollider::Mesh => (), }
}
}
pub type CustomTileColliderResource = Resource<CustomTileCollider>;
#[derive(Clone, PartialEq, Debug, Default, Visit, Reflect, TypeUuidProvider)]
#[type_uuid(id = "118da556-a444-4bd9-bd88-12d78d26107f")]
pub struct CustomTileCollider {
pub vertices: Vec<Vector2<f32>>,
pub triangles: Vec<TriangleDefinition>,
}
impl ResourceData for CustomTileCollider {
fn type_uuid(&self) -> Uuid {
<Self as TypeUuidProvider>::type_uuid()
}
fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
let mut visitor = Visitor::new();
self.visit("CustomTileCollider", &mut visitor)?;
visitor.save_ascii_to_file(path)?;
Ok(())
}
fn can_be_saved(&self) -> bool {
false
}
fn try_clone_box(&self) -> Option<Box<dyn ResourceData>> {
Some(Box::new(self.clone()))
}
}
impl OrthoTransform for CustomTileCollider {
fn x_flipped(self) -> Self {
Self {
vertices: self
.vertices
.iter()
.map(|v| Vector2::new(1.0 - v.x, v.y))
.collect(),
..self
}
}
fn rotated(self, amount: i8) -> Self {
let center = Vector2::new(0.5, 0.5);
Self {
vertices: self
.vertices
.iter()
.map(|v| (v - center).rotated(amount) + center)
.collect(),
..self
}
}
}
impl CustomTileCollider {
pub fn build_collider_shape(
&self,
transform: &Matrix4<f32>,
position: Vector3<f32>,
vertices: &mut Vec<Point2<f32>>,
triangles: &mut Vec<[u32; 3]>,
) {
let origin = vertices.len() as u32;
triangles.extend(self.triangles.iter().map(|d| d.0.map(|i| i + origin)));
vertices.extend(self.vertices.iter().map(|p| {
transform
.transform_point(&Point3::from(position + p.to_homogeneous()))
.xy()
}));
}
}
impl Display for CustomTileCollider {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut first = true;
for v in self.vertices.iter() {
if !first {
write!(f, " ")?;
}
first = false;
write!(f, "({}, {})", v.x, v.y)?;
}
for TriangleDefinition(t) in self.triangles.iter() {
if !first {
write!(f, " ")?;
}
first = false;
write!(f, "[{}, {}, {}]", t[0], t[1], t[2])?;
}
Ok(())
}
}
#[derive(Debug)]
pub enum CustomTileColliderStrError {
GroupTooShort,
GroupTooLong(usize),
MissingNumber,
IndexOutOfBounds(u32),
IndexParseError(ParseIntError),
CoordinateParseError(ParseFloatError),
}
impl From<ParseIntError> for CustomTileColliderStrError {
fn from(value: ParseIntError) -> Self {
Self::IndexParseError(value)
}
}
impl From<ParseFloatError> for CustomTileColliderStrError {
fn from(value: ParseFloatError) -> Self {
Self::CoordinateParseError(value)
}
}
impl Error for CustomTileColliderStrError {}
impl Display for CustomTileColliderStrError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
CustomTileColliderStrError::GroupTooShort => {
write!(f, "Each group must have at least 2 numbers.")
}
CustomTileColliderStrError::GroupTooLong(n) => {
write!(f, "A group has {n} numbers. No group may be longer than 3.")
}
CustomTileColliderStrError::IndexOutOfBounds(n) => {
write!(
f,
"Triangle index {n} does not match any of the given vertices."
)
}
CustomTileColliderStrError::MissingNumber => {
write!(f, "Numbers in a group must be separated by commas.")
}
CustomTileColliderStrError::IndexParseError(parse_int_error) => {
write!(f, "Index parse failure: {parse_int_error}")
}
CustomTileColliderStrError::CoordinateParseError(parse_float_error) => {
write!(f, "Coordinate parse failure: {parse_float_error}")
}
}
}
}
impl FromStr for CustomTileCollider {
type Err = CustomTileColliderStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut group = Vec::<&str>::default();
let mut ready = true;
let mut vertices = Vec::<Vector2<f32>>::default();
let mut triangles = Vec::<TriangleDefinition>::default();
for token in TokenIter::new(s) {
if ready {
if token != "," {
group.push(token);
ready = false;
} else {
return Err(CustomTileColliderStrError::MissingNumber);
}
} else if token != "," {
process_group(&group, &mut vertices, &mut triangles)?;
group.clear();
group.push(token);
} else {
ready = true;
}
}
if !group.is_empty() {
process_group(&group, &mut vertices, &mut triangles)?;
}
let len = vertices.len() as u32;
for TriangleDefinition(tri) in triangles.iter() {
for &n in tri.iter() {
if n >= len {
return Err(CustomTileColliderStrError::IndexOutOfBounds(n));
}
}
}
Ok(Self {
vertices,
triangles,
})
}
}
fn process_group(
group: &[&str],
vertices: &mut Vec<Vector2<f32>>,
triangles: &mut Vec<TriangleDefinition>,
) -> Result<(), CustomTileColliderStrError> {
use CustomTileColliderStrError as Error;
let len = group.len();
if len < 2 {
return Err(Error::GroupTooShort);
} else if len > 3 {
return Err(Error::GroupTooLong(group.len()));
} else if len == 2 {
let v = Vector2::new(parse_f32(group[0])?, parse_f32(group[1])?);
vertices.push(v);
} else if len == 3 {
let t = TriangleDefinition([
u32::from_str(group[0])?,
u32::from_str(group[1])?,
u32::from_str(group[2])?,
]);
triangles.push(t);
}
Ok(())
}
fn parse_f32(source: &str) -> Result<f32, ParseFloatError> {
let value = f32::from_str(source)?;
f32::from_str(&format!("{value:.3}"))
}
struct TokenIter<'a> {
source: &'a str,
position: usize,
}
impl<'a> TokenIter<'a> {
fn new(source: &'a str) -> Self {
Self {
source,
position: 0,
}
}
}
fn is_number_char(c: char) -> bool {
c.is_numeric() || c == '.' || c == '-'
}
fn is_ignore_char(c: char) -> bool {
!is_number_char(c) && c != ','
}
impl<'a> Iterator for TokenIter<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
let rest = self.source.get(self.position..)?;
if rest.is_empty() {
return None;
}
let mut initial_ignore = true;
let mut start = 0;
for (i, c) in rest.char_indices() {
if initial_ignore {
if is_ignore_char(c) {
continue;
} else {
initial_ignore = false;
start = i;
}
}
if c == ',' {
if i == start {
self.position += i + 1;
return Some(&rest[start..i + 1]);
} else {
self.position += i;
return Some(&rest[start..i]);
}
} else if is_ignore_char(c) {
self.position += i + 1;
return Some(&rest[start..i]);
}
}
if initial_ignore {
return None;
}
self.position = self.source.len();
Some(&rest[start..])
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty() {
let mut iter = TokenIter::new("");
assert_eq!(iter.next(), None);
}
#[test]
fn empty2() {
let mut iter = TokenIter::new(" ");
assert_eq!(iter.next(), None);
}
#[test]
fn comma() {
let mut iter = TokenIter::new("0,1");
assert_eq!(iter.next().unwrap(), "0");
assert_eq!(iter.next().unwrap(), ",");
assert_eq!(iter.next().unwrap(), "1");
assert_eq!(iter.next(), None);
}
#[test]
fn comma2() {
let mut iter = TokenIter::new(" 0.4 , -1 ");
assert_eq!(iter.next().unwrap(), "0.4");
assert_eq!(iter.next().unwrap(), ",");
assert_eq!(iter.next().unwrap(), "-1");
assert_eq!(iter.next(), None);
}
#[test]
fn comma3() {
let mut iter = TokenIter::new(",, ,");
assert_eq!(iter.next().unwrap(), ",");
assert_eq!(iter.next().unwrap(), ",");
assert_eq!(iter.next().unwrap(), ",");
assert_eq!(iter.next(), None);
}
#[test]
fn number() {
let mut iter = TokenIter::new("0");
assert_eq!(iter.next().unwrap(), "0");
assert_eq!(iter.next(), None);
}
#[test]
fn number2() {
let mut iter = TokenIter::new("-3.14");
assert_eq!(iter.next().unwrap(), "-3.14");
assert_eq!(iter.next(), None);
}
#[test]
fn number3() {
let mut iter = TokenIter::new(" -3.14 ");
assert_eq!(iter.next().unwrap(), "-3.14");
assert_eq!(iter.next(), None);
}
#[test]
fn collider() {
let col = CustomTileCollider::from_str("0,0; 1,1; 1,0; 0,1,2").unwrap();
assert_eq!(col.vertices.len(), 3);
assert_eq!(col.vertices[0], Vector2::new(0.0, 0.0));
assert_eq!(col.vertices[1], Vector2::new(1.0, 1.0));
assert_eq!(col.vertices[2], Vector2::new(1.0, 0.0));
assert_eq!(col.triangles.len(), 1);
assert_eq!(col.triangles[0], TriangleDefinition([0, 1, 2]));
}
#[test]
fn collider_display() {
let col = CustomTileCollider::from_str("0,0; 1,1; 1,0.333; 0,1,2").unwrap();
assert_eq!(col.to_string(), "(0, 0) (1, 1) (1, 0.333) [0, 1, 2]");
}
}