use bevy::prelude::*;
use std::{cmp::Ordering, sync::Arc, time::Duration};
pub struct PathPlugin;
impl Plugin for PathPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, (tick_path_timer, update_entity_position))
.insert_resource(PathTimer::default());
}
}
pub struct PathDebugPlugin;
impl Plugin for PathDebugPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, debug_render_paths);
}
}
fn should_remove(p1: &Vec2, p2: &Vec2, p3: &Vec2, puncture_points: &[PuncturePoint]) -> bool {
puncture_points.iter().all(|p| p.should_remove(p1, p2, p3))
}
#[derive(Resource)]
pub struct PathTimer {
pub timer: Timer,
}
impl Default for PathTimer {
fn default() -> Self {
Self {
timer: Timer::new(Duration::from_millis(250), TimerMode::Repeating),
}
}
}
fn tick_path_timer(mut path_timer: ResMut<PathTimer>, time: Res<Time>) {
path_timer.timer.tick(time.delta());
}
fn update_entity_position(
mut path_query: Query<(&mut PathType, &Transform)>,
) {
for (mut path_type, transform) in path_query.iter_mut() {
let current_position = transform.translation.truncate();
if ¤t_position != path_type.current_path.end() {
path_type.push(¤t_position);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Component)]
pub struct PuncturePoint {
position: Vec2,
name: char,
}
impl PuncturePoint {
pub const fn new(position: Vec2, name: char) -> Self {
Self {
position,
name: name.to_ascii_uppercase(),
}
}
pub const fn position(&self) -> &Vec2 {
&self.position
}
pub const fn name(&self) -> char {
self.name
}
fn is_in_triangle(&self, p1: &Vec2, p2: &Vec2, p3: &Vec2) -> bool {
let p = self.position();
let denom = (p2.y - p3.y).mul_add(p1.x - p3.x, (p3.x - p2.x) * (p1.y - p3.y));
if denom.abs() <= f32::EPSILON {
return false;
}
let a = (p2.y - p3.y).mul_add(p.x - p3.x, (p3.x - p2.x) * (p.y - p3.y)) / denom;
let b = (p3.y - p1.y).mul_add(p.x - p3.x, (p1.x - p3.x) * (p.y - p3.y)) / denom;
let c = 1.0 - a - b;
[a, b, c].iter().all(|x| (0.0..1.0).contains(x))
}
fn should_remove(&self, p1: &Vec2, p2: &Vec2, p3: &Vec2) -> bool {
let x = self.position().x;
!(self.is_in_triangle(p1, p2, p3)
|| ((p1.x..p2.x).contains(&x) && p2.x < p3.x && (x - p2.x).abs() < 1e-3)
|| ((p2.x..p1.x).contains(&x) && p3.x < p2.x && (x - p2.x).abs() < 1e-3))
}
fn winding_update(&self, start: &Vec2, end: &Vec2) -> Option<i32> {
let position = self.position();
let cross_product = (end.y - start.y).mul_add(
position.x - start.x,
-((position.y - start.y) * (end.x - start.x)),
);
if cross_product > 0. && (start.x..end.x).contains(&position.x) {
return Some(1);
}
if cross_product < 0. && (end.x..start.x).contains(&position.x) {
return Some(-1);
}
None
}
}
#[derive(Debug, Clone, PartialEq, Component)]
pub struct PLPath {
nodes: Vec<Vec2>,
}
impl PLPath {
fn start(&self) -> &Vec2 {
self.nodes.first().expect("Couldn't get the start point")
}
fn end(&self) -> &Vec2 {
self.nodes.last().expect("Couldn't get the end point")
}
fn push(&mut self, position: &Vec2) {
self.nodes.push(*position);
}
pub fn push_transform(&mut self, transform: Transform) {
self.nodes.push(transform.translation.truncate());
}
pub fn new(nodes: impl Into<Vec<Vec2>>) -> Self {
Self {
nodes: nodes.into(),
}
}
pub fn line(start: Vec2, end: Vec2) -> Self {
Self {
nodes: vec![start, end],
}
}
pub fn reverse(&self) -> Self {
let mut reversed_nodes = self.nodes.clone();
reversed_nodes.reverse();
Self {
nodes: reversed_nodes,
}
}
pub fn concatenate(&self, other: &Self) -> Self {
let mut nodes = self.nodes.clone();
nodes.extend(other.nodes.clone());
Self { nodes }
}
fn to_segment2d_iter(&self) -> impl Iterator<Item = (Segment2d, Vec2)> + '_ {
let last = if self.start() != self.end() {
Some(Segment2d::from_points(*self.end(), *self.start()))
} else {
None
};
self.nodes
.windows(2)
.filter_map(|pair| {
let point1 = pair[0];
let point2 = pair[1];
if point1 == point2 {
None
} else {
let segment = Segment2d::from_points(point1, point2);
Some(segment)
}
})
.chain(last)
}
}
#[derive(Debug, Clone, Component)]
pub struct PathType {
current_path: PLPath,
puncture_points: Arc<[PuncturePoint]>,
word: String,
}
impl PathType {
pub fn word_as_str(&self) -> &str {
&self.word
}
pub fn word(&self) -> String {
self.word.clone()
}
pub fn new(start: Vec2, puncture_points: Vec<PuncturePoint>) -> Self {
Self {
current_path: PLPath::new(vec![start]),
puncture_points: puncture_points.into(),
word: String::new(),
}
}
pub fn from_path(path: PLPath, puncture_points: Arc<[PuncturePoint]>) -> Self {
let mut path_type = Self {
current_path: path,
puncture_points,
word: String::new(),
};
path_type.update_word();
path_type
}
#[must_use]
pub fn concatenate(&self, other: &PLPath) -> Self {
Self::from_path(
self.current_path.concatenate(other),
self.puncture_points.clone(),
)
}
fn pop(&mut self) -> Option<Vec2> {
self.current_path.nodes.pop()
}
pub fn push(&mut self, point: &Vec2) {
if let [.., p1, p2] = &self.current_path.nodes[..] {
if should_remove(p1, p2, point, &self.puncture_points) {
self.pop();
self.push(point);
} else {
self.current_path.push(point);
}
} else {
self.current_path.push(point);
}
self.update_word();
}
pub fn update_word(&mut self) -> String {
let mut word = String::new();
let full_loop: Vec<&Vec2> = self
.current_path
.nodes
.iter()
.chain(std::iter::once(self.current_path.start()))
.collect();
for segment in full_loop.windows(2) {
let (start, end) = (segment[0], segment[1]);
let punctures: Vec<&PuncturePoint> = match start.x.partial_cmp(&end.x) {
Some(Ordering::Less) => self.puncture_points.iter().collect(),
Some(Ordering::Greater) => self
.puncture_points
.iter()
.collect(),
_ => continue,
};
for puncture in punctures {
if let Some(n) = puncture.winding_update(start, end) {
match n {
1 => word.push(puncture.name.to_ascii_lowercase()),
-1 => word.push(puncture.name.to_ascii_uppercase()),
_ => {}
}
}
}
}
simplify_word(&mut word);
self.word = word.clone();
word
}
}
fn simplify_word(word: &mut String) {
let mut i = 0;
while i + 1 < word.len() {
let a = word.as_bytes()[i] as char;
let b = word.as_bytes()[i + 1] as char;
if a.to_ascii_uppercase() == b.to_ascii_uppercase() && a != b {
word.drain(i..i + 2);
i = i.saturating_sub(1);
} else {
i += 1;
}
}
}
fn debug_render_paths(path_types: Query<&PathType>, mut gizmos: Gizmos) {
for path_type in path_types.iter() {
if path_type.current_path.nodes.len() > 1 {
for segment in path_type.current_path.to_segment2d_iter() {
gizmos.primitive_2d(segment.0, segment.1, 0.0, Color::WHITE);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_point_in_triangle() {
let p1 = &Vec2::new(0.0, 0.0);
let p2 = &Vec2::new(4.0, 0.0);
let p3 = &Vec2::new(2.0, 4.0);
let puncture_point_inside = PuncturePoint::new(*p1, 'A');
let puncture_point_inside_2 = PuncturePoint::new(*p2, 'B');
let puncture_point_outside = PuncturePoint::new(*p3, 'A');
assert!(puncture_point_inside.is_in_triangle(p1, p2, p3));
assert!(puncture_point_inside_2.is_in_triangle(p1, p2, p3));
assert!(!puncture_point_outside.is_in_triangle(p1, p2, p3));
}
#[test]
fn test_simplify_word_with_multibyte_chars() {
let mut word = "ßAa".to_string();
simplify_word(&mut word);
assert_eq!(word, "ß");
}
}