1use bevy::prelude::*;
2use std::{cmp::Ordering, sync::Arc, time::Duration};
3
4pub struct PathPlugin;
6
7impl Plugin for PathPlugin {
8 fn build(&self, app: &mut App) {
9 app.add_systems(Update, (tick_path_timer, update_entity_position))
10 .insert_resource(PathTimer::default());
11 }
12}
13
14pub struct PathDebugPlugin;
17
18impl Plugin for PathDebugPlugin {
19 fn build(&self, app: &mut App) {
20 app.add_systems(Update, debug_render_paths);
21 }
22}
23
24fn should_remove(p1: &Vec2, p2: &Vec2, p3: &Vec2, puncture_points: &[PuncturePoint]) -> bool {
26 puncture_points.iter().all(|p| p.should_remove(p1, p2, p3))
27}
28
29#[derive(Resource)]
31pub struct PathTimer {
32 pub timer: Timer,
33}
34
35impl Default for PathTimer {
36 fn default() -> Self {
37 Self {
38 timer: Timer::new(Duration::from_millis(250), TimerMode::Repeating),
39 }
40 }
41}
42
43fn tick_path_timer(mut path_timer: ResMut<PathTimer>, time: Res<Time>) {
45 path_timer.timer.tick(time.delta());
46}
47
48fn update_entity_position(
50 mut path_query: Query<(&mut PathType, &Transform)>,
51 ) {
53 for (mut path_type, transform) in path_query.iter_mut() {
55 let current_position = transform.translation.truncate();
56 if ¤t_position != path_type.current_path.end() {
57 path_type.push(¤t_position);
58 }
59 }
60 }
62
63#[derive(Debug, Clone, Copy, PartialEq, Component)]
88pub struct PuncturePoint {
89 position: Vec2,
90 name: char,
91}
92
93impl PuncturePoint {
94 pub const fn new(position: Vec2, name: char) -> Self {
96 Self {
97 position,
98 name: name.to_ascii_uppercase(),
99 }
100 }
101
102 pub const fn position(&self) -> &Vec2 {
104 &self.position
105 }
106
107 pub const fn name(&self) -> char {
109 self.name
110 }
111
112 fn is_in_triangle(&self, p1: &Vec2, p2: &Vec2, p3: &Vec2) -> bool {
114 let p = self.position();
115 let denom = (p2.y - p3.y).mul_add(p1.x - p3.x, (p3.x - p2.x) * (p1.y - p3.y));
116 if denom.abs() <= f32::EPSILON {
117 return false;
118 }
119 let a = (p2.y - p3.y).mul_add(p.x - p3.x, (p3.x - p2.x) * (p.y - p3.y)) / denom;
120 let b = (p3.y - p1.y).mul_add(p.x - p3.x, (p1.x - p3.x) * (p.y - p3.y)) / denom;
121 let c = 1.0 - a - b;
122 [a, b, c].iter().all(|x| (0.0..1.0).contains(x))
123 }
124
125 fn should_remove(&self, p1: &Vec2, p2: &Vec2, p3: &Vec2) -> bool {
127 let x = self.position().x;
128 !(self.is_in_triangle(p1, p2, p3)
129 || ((p1.x..p2.x).contains(&x) && p2.x < p3.x && (x - p2.x).abs() < 1e-3)
130 || ((p2.x..p1.x).contains(&x) && p3.x < p2.x && (x - p2.x).abs() < 1e-3))
131 }
133
134 fn winding_update(&self, start: &Vec2, end: &Vec2) -> Option<i32> {
140 let position = self.position();
141 let cross_product = (end.y - start.y).mul_add(
142 position.x - start.x,
143 -((position.y - start.y) * (end.x - start.x)),
144 );
145 if cross_product > 0. && (start.x..end.x).contains(&position.x) {
147 return Some(1);
148 }
149 if cross_product < 0. && (end.x..start.x).contains(&position.x) {
150 return Some(-1);
151 }
152 None
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Component)]
157pub struct PLPath {
158 nodes: Vec<Vec2>,
159}
160
161impl PLPath {
162 fn start(&self) -> &Vec2 {
167 self.nodes.first().expect("Couldn't get the start point")
168 }
169
170 fn end(&self) -> &Vec2 {
175 self.nodes.last().expect("Couldn't get the end point")
176 }
177
178 fn push(&mut self, position: &Vec2) {
180 self.nodes.push(*position);
181 }
182 pub fn push_transform(&mut self, transform: Transform) {
184 self.nodes.push(transform.translation.truncate());
185 }
186
187 pub fn new(nodes: impl Into<Vec<Vec2>>) -> Self {
189 Self {
190 nodes: nodes.into(),
191 }
192 }
193
194 pub fn line(start: Vec2, end: Vec2) -> Self {
196 Self {
197 nodes: vec![start, end],
198 }
199 }
200
201 pub fn reverse(&self) -> Self {
203 let mut reversed_nodes = self.nodes.clone();
204 reversed_nodes.reverse();
205 Self {
206 nodes: reversed_nodes,
207 }
208 }
209
210 pub fn concatenate(&self, other: &Self) -> Self {
212 let mut nodes = self.nodes.clone();
213 nodes.extend(other.nodes.clone());
214 Self { nodes }
215 }
216
217 fn to_segment2d_iter(&self) -> impl Iterator<Item = (Segment2d, Vec2)> + '_ {
220 let last = if self.start() != self.end() {
221 Some(Segment2d::from_points(*self.end(), *self.start()))
222 } else {
223 None
224 };
225 self.nodes
226 .windows(2)
227 .filter_map(|pair| {
228 let point1 = pair[0];
229 let point2 = pair[1];
230 if point1 == point2 {
231 None
232 } else {
233 let segment = Segment2d::from_points(point1, point2);
234 Some(segment)
235 }
236 })
237 .chain(last)
238 }
239}
240
241#[derive(Debug, Clone, Component)]
279pub struct PathType {
280 current_path: PLPath,
281 puncture_points: Arc<[PuncturePoint]>,
282 word: String,
283}
284
285impl PathType {
286 pub fn word_as_str(&self) -> &str {
287 &self.word
288 }
289 pub fn word(&self) -> String {
290 self.word.clone()
291 }
292
293 pub fn new(start: Vec2, puncture_points: Vec<PuncturePoint>) -> Self {
294 Self {
295 current_path: PLPath::new(vec![start]),
296 puncture_points: puncture_points.into(),
297 word: String::new(),
298 }
299 }
300
301 pub fn from_path(path: PLPath, puncture_points: Arc<[PuncturePoint]>) -> Self {
302 let mut path_type = Self {
303 current_path: path,
304 puncture_points,
305 word: String::new(),
306 };
307 path_type.update_word();
308 path_type
309 }
310
311 #[must_use]
312 pub fn concatenate(&self, other: &PLPath) -> Self {
313 Self::from_path(
314 self.current_path.concatenate(other),
315 self.puncture_points.clone(),
316 )
317 }
318
319 fn pop(&mut self) -> Option<Vec2> {
320 self.current_path.nodes.pop()
321 }
322
323 pub fn push(&mut self, point: &Vec2) {
325 if let [.., p1, p2] = &self.current_path.nodes[..] {
326 if should_remove(p1, p2, point, &self.puncture_points) {
327 self.pop();
328 self.push(point);
329 } else {
330 self.current_path.push(point);
331 }
332 } else {
333 self.current_path.push(point);
334 }
335 self.update_word();
336 }
337
338 pub fn update_word(&mut self) -> String {
341 let mut word = String::new();
342 let full_loop: Vec<&Vec2> = self
343 .current_path
344 .nodes
345 .iter()
346 .chain(std::iter::once(self.current_path.start()))
347 .collect();
348 for segment in full_loop.windows(2) {
349 let (start, end) = (segment[0], segment[1]);
350 let punctures: Vec<&PuncturePoint> = match start.x.partial_cmp(&end.x) {
351 Some(Ordering::Less) => self.puncture_points.iter().collect(),
352 Some(Ordering::Greater) => self
353 .puncture_points
354 .iter()
355 .collect(),
357 _ => continue,
358 };
359 for puncture in punctures {
360 if let Some(n) = puncture.winding_update(start, end) {
361 match n {
362 1 => word.push(puncture.name.to_ascii_lowercase()),
363 -1 => word.push(puncture.name.to_ascii_uppercase()),
364 _ => {}
365 }
366 }
367 }
368 }
369
370 simplify_word(&mut word);
371 self.word = word.clone();
372 word
373 }
374}
375
376fn simplify_word(word: &mut String) {
377 let mut i = 0;
378 while i + 1 < word.len() {
379 let a = word.as_bytes()[i] as char;
380 let b = word.as_bytes()[i + 1] as char;
381
382 if a.to_ascii_uppercase() == b.to_ascii_uppercase() && a != b {
383 word.drain(i..i + 2);
384 i = i.saturating_sub(1);
385 } else {
386 i += 1;
387 }
388 }
389}
390
391fn debug_render_paths(path_types: Query<&PathType>, mut gizmos: Gizmos) {
393 for path_type in path_types.iter() {
394 if path_type.current_path.nodes.len() > 1 {
395 for segment in path_type.current_path.to_segment2d_iter() {
396 gizmos.primitive_2d(segment.0, segment.1, 0.0, Color::WHITE);
397 }
398 }
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use super::*;
405
406 #[test]
407 fn test_is_point_in_triangle() {
408 let p1 = &Vec2::new(0.0, 0.0);
409 let p2 = &Vec2::new(4.0, 0.0);
410 let p3 = &Vec2::new(2.0, 4.0);
411
412 let puncture_point_inside = PuncturePoint::new(*p1, 'A');
413 let puncture_point_inside_2 = PuncturePoint::new(*p2, 'B');
414 let puncture_point_outside = PuncturePoint::new(*p3, 'A');
415
416 assert!(puncture_point_inside.is_in_triangle(p1, p2, p3));
417 assert!(puncture_point_inside_2.is_in_triangle(p1, p2, p3));
418 assert!(!puncture_point_outside.is_in_triangle(p1, p2, p3));
419 }
420
421 #[test]
422 fn test_simplify_word_with_multibyte_chars() {
423 let mut word = "ßAa".to_string();
424 simplify_word(&mut word);
425 assert_eq!(word, "ß");
426 }
427}