1use crate::{
28 asset::{Resource, ResourceData},
29 core::{
30 algebra::{Matrix4, Point2, Point3, Vector2},
31 reflect::prelude::*,
32 type_traits::prelude::*,
33 visitor::prelude::*,
34 },
35};
36use std::{
37 error::Error,
38 fmt::{Debug, Display, Formatter},
39 num::{ParseFloatError, ParseIntError},
40 path::Path,
41 str::FromStr,
42};
43use strum_macros::{AsRefStr, EnumString, VariantNames};
44
45use super::*;
46
47#[derive(
49 Clone,
50 Hash,
51 PartialEq,
52 Eq,
53 Default,
54 Visit,
55 Reflect,
56 AsRefStr,
57 EnumString,
58 VariantNames,
59 TypeUuidProvider,
60)]
61#[type_uuid(id = "04a44fec-394f-4497-97d5-fe9e6f915831")]
62pub enum TileCollider {
63 #[default]
65 None,
66 Rectangle,
68 Custom(CustomTileColliderResource),
70 Mesh,
72}
73
74impl Default for &TileCollider {
75 fn default() -> Self {
76 &TileCollider::None
77 }
78}
79
80impl Debug for TileCollider {
81 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
82 match self {
83 Self::None => write!(f, "None"),
84 Self::Rectangle => write!(f, "Rectangle"),
85 Self::Custom(r) => write!(f, "Custom({})", r.data_ref().deref()),
86 Self::Mesh => write!(f, "Mesh"),
87 }
88 }
89}
90
91impl OrthoTransform for TileCollider {
92 fn x_flipped(self) -> Self {
93 if let Self::Custom(collider) = self {
94 let collider = collider.data_ref().clone();
95 Self::Custom(Resource::new_ok(
96 ResourceKind::Embedded,
97 collider.x_flipped(),
98 ))
99 } else {
100 self
101 }
102 }
103 fn rotated(self, amount: i8) -> Self {
104 if let Self::Custom(collider) = self {
105 let collider = collider.data_ref().clone();
106 Self::Custom(Resource::new_ok(
107 ResourceKind::Embedded,
108 collider.rotated(amount),
109 ))
110 } else {
111 self
112 }
113 }
114}
115
116impl TileCollider {
117 pub fn is_none(&self) -> bool {
119 matches!(self, TileCollider::None)
120 }
121 pub fn is_rectangle(&self) -> bool {
123 matches!(self, TileCollider::Rectangle)
124 }
125 pub fn is_custom(&self) -> bool {
127 matches!(self, TileCollider::Custom(_))
128 }
129
130 pub fn build_collider_shape(
132 &self,
133 transform: &Matrix4<f32>,
134 position: Vector3<f32>,
135 vertices: &mut Vec<Point2<f32>>,
136 triangles: &mut Vec<[u32; 3]>,
137 ) {
138 match self {
139 TileCollider::None => (),
140 TileCollider::Rectangle => {
141 let origin = vertices.len() as u32;
142 for (dx, dy) in [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] {
143 let offset = Vector3::new(dx, dy, 0.0);
144 let point = Point3::from(position + offset);
145 vertices.push(transform.transform_point(&point).xy());
146 }
147
148 triangles.push([origin, origin + 1, origin + 2]);
149 triangles.push([origin, origin + 2, origin + 3]);
150 }
151 TileCollider::Custom(resource) => {
152 resource
153 .data_ref()
154 .build_collider_shape(transform, position, vertices, triangles);
155 }
156 TileCollider::Mesh => (), }
158 }
159}
160
161pub type CustomTileColliderResource = Resource<CustomTileCollider>;
163#[derive(Clone, PartialEq, Debug, Default, Visit, Reflect, TypeUuidProvider)]
172#[type_uuid(id = "118da556-a444-4bd9-bd88-12d78d26107f")]
173pub struct CustomTileCollider {
174 pub vertices: Vec<Vector2<f32>>,
176 pub triangles: Vec<TriangleDefinition>,
178}
179
180impl ResourceData for CustomTileCollider {
181 fn type_uuid(&self) -> Uuid {
182 <Self as TypeUuidProvider>::type_uuid()
183 }
184
185 fn save(&mut self, path: &Path) -> Result<(), Box<dyn Error>> {
186 let mut visitor = Visitor::new();
187 self.visit("CustomTileCollider", &mut visitor)?;
188 visitor.save_binary(path)?;
189 Ok(())
190 }
191
192 fn can_be_saved(&self) -> bool {
193 false
194 }
195}
196
197impl OrthoTransform for CustomTileCollider {
198 fn x_flipped(self) -> Self {
199 Self {
200 vertices: self
201 .vertices
202 .iter()
203 .map(|v| Vector2::new(1.0 - v.x, v.y))
204 .collect(),
205 ..self
206 }
207 }
208
209 fn rotated(self, amount: i8) -> Self {
210 let center = Vector2::new(0.5, 0.5);
211 Self {
212 vertices: self
213 .vertices
214 .iter()
215 .map(|v| (v - center).rotated(amount) + center)
216 .collect(),
217 ..self
218 }
219 }
220}
221
222impl CustomTileCollider {
223 pub fn build_collider_shape(
229 &self,
230 transform: &Matrix4<f32>,
231 position: Vector3<f32>,
232 vertices: &mut Vec<Point2<f32>>,
233 triangles: &mut Vec<[u32; 3]>,
234 ) {
235 let origin = vertices.len() as u32;
236 triangles.extend(self.triangles.iter().map(|d| d.0.map(|i| i + origin)));
237 vertices.extend(self.vertices.iter().map(|p| {
238 transform
239 .transform_point(&Point3::from(position + p.to_homogeneous()))
240 .xy()
241 }));
242 }
243}
244
245impl Display for CustomTileCollider {
246 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
247 let mut first = true;
248 for v in self.vertices.iter() {
249 if !first {
250 write!(f, " ")?;
251 }
252 first = false;
253 write!(f, "({}, {})", v.x, v.y)?;
254 }
255 for TriangleDefinition(t) in self.triangles.iter() {
256 if !first {
257 write!(f, " ")?;
258 }
259 first = false;
260 write!(f, "[{}, {}, {}]", t[0], t[1], t[2])?;
261 }
262 Ok(())
263 }
264}
265
266#[derive(Debug)]
268pub enum CustomTileColliderStrError {
269 GroupTooShort,
271 GroupTooLong(usize),
273 MissingNumber,
275 IndexOutOfBounds(u32),
278 IndexParseError(ParseIntError),
280 CoordinateParseError(ParseFloatError),
282}
283
284impl From<ParseIntError> for CustomTileColliderStrError {
285 fn from(value: ParseIntError) -> Self {
286 Self::IndexParseError(value)
287 }
288}
289
290impl From<ParseFloatError> for CustomTileColliderStrError {
291 fn from(value: ParseFloatError) -> Self {
292 Self::CoordinateParseError(value)
293 }
294}
295
296impl Error for CustomTileColliderStrError {}
297
298impl Display for CustomTileColliderStrError {
299 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
300 match self {
301 CustomTileColliderStrError::GroupTooShort => {
302 write!(f, "Each group must have at least 2 numbers.")
303 }
304 CustomTileColliderStrError::GroupTooLong(n) => {
305 write!(f, "A group has {n} numbers. No group may be longer than 3.")
306 }
307 CustomTileColliderStrError::IndexOutOfBounds(n) => {
308 write!(
309 f,
310 "Triangle index {n} does not match any of the given vertices."
311 )
312 }
313 CustomTileColliderStrError::MissingNumber => {
314 write!(f, "Numbers in a group must be separated by commas.")
315 }
316 CustomTileColliderStrError::IndexParseError(parse_int_error) => {
317 write!(f, "Index parse failure: {parse_int_error}")
318 }
319 CustomTileColliderStrError::CoordinateParseError(parse_float_error) => {
320 write!(f, "Coordinate parse failure: {parse_float_error}")
321 }
322 }
323 }
324}
325
326impl FromStr for CustomTileCollider {
327 type Err = CustomTileColliderStrError;
328
329 fn from_str(s: &str) -> Result<Self, Self::Err> {
330 let mut group = Vec::<&str>::default();
331 let mut ready = true;
332 let mut vertices = Vec::<Vector2<f32>>::default();
333 let mut triangles = Vec::<TriangleDefinition>::default();
334 for token in TokenIter::new(s) {
335 if ready {
336 if token != "," {
337 group.push(token);
338 ready = false;
339 } else {
340 return Err(CustomTileColliderStrError::MissingNumber);
341 }
342 } else if token != "," {
343 process_group(&group, &mut vertices, &mut triangles)?;
344 group.clear();
345 group.push(token);
346 } else {
347 ready = true;
348 }
349 }
350 if !group.is_empty() {
351 process_group(&group, &mut vertices, &mut triangles)?;
352 }
353 let len = vertices.len() as u32;
354 for TriangleDefinition(tri) in triangles.iter() {
355 for &n in tri.iter() {
356 if n >= len {
357 return Err(CustomTileColliderStrError::IndexOutOfBounds(n));
358 }
359 }
360 }
361 Ok(Self {
362 vertices,
363 triangles,
364 })
365 }
366}
367
368fn process_group(
369 group: &[&str],
370 vertices: &mut Vec<Vector2<f32>>,
371 triangles: &mut Vec<TriangleDefinition>,
372) -> Result<(), CustomTileColliderStrError> {
373 use CustomTileColliderStrError as Error;
374 let len = group.len();
375 if len < 2 {
376 return Err(Error::GroupTooShort);
377 } else if len > 3 {
378 return Err(Error::GroupTooLong(group.len()));
379 } else if len == 2 {
380 let v = Vector2::new(parse_f32(group[0])?, parse_f32(group[1])?);
381 vertices.push(v);
382 } else if len == 3 {
383 let t = TriangleDefinition([
384 u32::from_str(group[0])?,
385 u32::from_str(group[1])?,
386 u32::from_str(group[2])?,
387 ]);
388 triangles.push(t);
389 }
390 Ok(())
391}
392
393fn parse_f32(source: &str) -> Result<f32, ParseFloatError> {
394 let value = f32::from_str(source)?;
395 f32::from_str(&format!("{value:.3}"))
396}
397
398struct TokenIter<'a> {
399 source: &'a str,
400 position: usize,
401}
402
403impl<'a> TokenIter<'a> {
404 fn new(source: &'a str) -> Self {
405 Self {
406 source,
407 position: 0,
408 }
409 }
410}
411
412fn is_number_char(c: char) -> bool {
413 c.is_numeric() || c == '.' || c == '-'
414}
415
416fn is_ignore_char(c: char) -> bool {
417 !is_number_char(c) && c != ','
418}
419
420impl<'a> Iterator for TokenIter<'a> {
421 type Item = &'a str;
422
423 fn next(&mut self) -> Option<Self::Item> {
424 let rest = self.source.get(self.position..)?;
425 if rest.is_empty() {
426 return None;
427 }
428 let mut initial_ignore = true;
429 let mut start = 0;
430 for (i, c) in rest.char_indices() {
431 if initial_ignore {
432 if is_ignore_char(c) {
433 continue;
434 } else {
435 initial_ignore = false;
436 start = i;
437 }
438 }
439 if c == ',' {
440 if i == start {
441 self.position += i + 1;
442 return Some(&rest[start..i + 1]);
443 } else {
444 self.position += i;
445 return Some(&rest[start..i]);
446 }
447 } else if is_ignore_char(c) {
448 self.position += i + 1;
449 return Some(&rest[start..i]);
450 }
451 }
452 if initial_ignore {
453 return None;
454 }
455 self.position = self.source.len();
456 Some(&rest[start..])
457 }
458}
459
460#[cfg(test)]
461mod tests {
462 use super::*;
463
464 #[test]
465 fn empty() {
466 let mut iter = TokenIter::new("");
467 assert_eq!(iter.next(), None);
468 }
469 #[test]
470 fn empty2() {
471 let mut iter = TokenIter::new(" ");
472 assert_eq!(iter.next(), None);
473 }
474
475 #[test]
476 fn comma() {
477 let mut iter = TokenIter::new("0,1");
478 assert_eq!(iter.next().unwrap(), "0");
479 assert_eq!(iter.next().unwrap(), ",");
480 assert_eq!(iter.next().unwrap(), "1");
481 assert_eq!(iter.next(), None);
482 }
483 #[test]
484 fn comma2() {
485 let mut iter = TokenIter::new(" 0.4 , -1 ");
486 assert_eq!(iter.next().unwrap(), "0.4");
487 assert_eq!(iter.next().unwrap(), ",");
488 assert_eq!(iter.next().unwrap(), "-1");
489 assert_eq!(iter.next(), None);
490 }
491 #[test]
492 fn comma3() {
493 let mut iter = TokenIter::new(",, ,");
494 assert_eq!(iter.next().unwrap(), ",");
495 assert_eq!(iter.next().unwrap(), ",");
496 assert_eq!(iter.next().unwrap(), ",");
497 assert_eq!(iter.next(), None);
498 }
499 #[test]
500 fn number() {
501 let mut iter = TokenIter::new("0");
502 assert_eq!(iter.next().unwrap(), "0");
503 assert_eq!(iter.next(), None);
504 }
505 #[test]
506 fn number2() {
507 let mut iter = TokenIter::new("-3.14");
508 assert_eq!(iter.next().unwrap(), "-3.14");
509 assert_eq!(iter.next(), None);
510 }
511 #[test]
512 fn number3() {
513 let mut iter = TokenIter::new(" -3.14 ");
514 assert_eq!(iter.next().unwrap(), "-3.14");
515 assert_eq!(iter.next(), None);
516 }
517 #[test]
518 fn collider() {
519 let col = CustomTileCollider::from_str("0,0; 1,1; 1,0; 0,1,2").unwrap();
520 assert_eq!(col.vertices.len(), 3);
521 assert_eq!(col.vertices[0], Vector2::new(0.0, 0.0));
522 assert_eq!(col.vertices[1], Vector2::new(1.0, 1.0));
523 assert_eq!(col.vertices[2], Vector2::new(1.0, 0.0));
524 assert_eq!(col.triangles.len(), 1);
525 assert_eq!(col.triangles[0], TriangleDefinition([0, 1, 2]));
526 }
527 #[test]
528 fn collider_display() {
529 let col = CustomTileCollider::from_str("0,0; 1,1; 1,0.333; 0,1,2").unwrap();
530 assert_eq!(col.to_string(), "(0, 0) (1, 1) (1, 0.333) [0, 1, 2]");
531 }
532}