#![allow(dead_code)]
use std::fmt::Write;
pub const SCALE: f64 = 8.0;
pub const ASPECT: f64 = 2.0;
pub const CURVE: f64 = 0.551915;
pub fn diagonal_angle() -> f64 {
(ASPECT).atan().to_degrees()
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vec2 {
pub x: f64,
pub y: f64,
}
impl Vec2 {
pub fn new(x: f64, y: f64) -> Self {
Self { x, y }
}
pub fn from_grid(x: i32, y: i32) -> Self {
Self {
x: (x as f64 + 1.0) * SCALE,
y: (y as f64 + 1.0) * SCALE * ASPECT,
}
}
pub fn from_grid_frac(x: f64, y: f64) -> Self {
Self {
x: (x + 1.0) * SCALE,
y: (y + 1.0) * SCALE * ASPECT,
}
}
pub fn offset(&self, dx: f64, dy: f64) -> Self {
Self {
x: self.x + dx * SCALE,
y: self.y + dy * SCALE * ASPECT,
}
}
pub fn offset_pixels(&self, dx: f64, dy: f64) -> Self {
Self {
x: self.x + dx,
y: self.y + dy,
}
}
pub fn coords(&self) -> String {
format!("{},{}", format_coord(self.x), format_coord(self.y))
}
pub fn to_svg(&self) -> String {
format!("{},{} ", format_coord(self.x), format_coord(self.y))
}
}
fn format_coord(x: f64) -> String {
let s = format!("{:.5}", x);
let s = s.trim_end_matches('0');
let s = s.trim_end_matches('.');
s.to_string()
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct PathStyle {
pub dashed: bool,
pub double: bool,
pub squiggle: bool,
}
#[derive(Debug, Clone)]
pub struct Path {
pub a: Vec2,
pub b: Vec2,
pub c: Option<Vec2>,
pub d: Option<Vec2>,
pub style: PathStyle,
}
impl Path {
pub fn line(a: Vec2, b: Vec2) -> Self {
Self {
a,
b,
c: None,
d: None,
style: PathStyle::default(),
}
}
pub fn line_from_grid(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
Self::line(Vec2::from_grid(x1, y1), Vec2::from_grid(x2, y2))
}
pub fn curve(a: Vec2, b: Vec2, c: Vec2, d: Vec2) -> Self {
Self {
a,
b,
c: Some(c),
d: Some(d),
style: PathStyle::default(),
}
}
pub fn with_dashed(mut self, dashed: bool) -> Self {
self.style.dashed = dashed;
self
}
pub fn with_double(mut self, double: bool) -> Self {
self.style.double = double;
self
}
pub fn with_squiggle(mut self, squiggle: bool) -> Self {
self.style.squiggle = squiggle;
self
}
pub fn is_degenerate(&self) -> bool {
(self.a.x - self.b.x).abs() < 0.01 && (self.a.y - self.b.y).abs() < 0.01
}
pub fn is_vertical(&self) -> bool {
self.c.is_none() && (self.a.x - self.b.x).abs() < 0.01
}
pub fn is_horizontal(&self) -> bool {
self.c.is_none() && (self.a.y - self.b.y).abs() < 0.01
}
pub fn is_diagonal(&self) -> bool {
if self.c.is_some() {
return false;
}
let dx = self.b.x - self.a.x;
let dy = self.b.y - self.a.y;
dx > 0.0 && dy < 0.0
}
pub fn is_back_diagonal(&self) -> bool {
if self.c.is_some() {
return false;
}
let dx = self.b.x - self.a.x;
let dy = self.b.y - self.a.y;
dx > 0.0 && dy > 0.0
}
pub fn is_curved(&self) -> bool {
self.c.is_some()
}
pub fn ends_at(&self, x: i32, y: i32) -> bool {
let target = Vec2::from_grid(x, y);
self.ends_at_point(target)
}
fn ends_at_point(&self, target: Vec2) -> bool {
let eps = SCALE / 2.0;
((self.a.x - target.x).abs() < eps && (self.a.y - target.y).abs() < eps)
|| ((self.b.x - target.x).abs() < eps && (self.b.y - target.y).abs() < eps)
}
pub fn up_ends_at(&self, x: i32, y: i32) -> bool {
self.up_ends_at_frac(x as f64, y as f64)
}
pub fn up_ends_at_frac(&self, x: f64, y: f64) -> bool {
if !self.is_vertical() {
return false;
}
let target = Vec2::from_grid_frac(x, y);
let eps = SCALE / 2.0;
let min_y = self.a.y.min(self.b.y);
(self.a.x - target.x).abs() < eps && (min_y - target.y).abs() < eps
}
pub fn down_ends_at(&self, x: i32, y: i32) -> bool {
self.down_ends_at_frac(x as f64, y as f64)
}
pub fn down_ends_at_frac(&self, x: f64, y: f64) -> bool {
if !self.is_vertical() {
return false;
}
let target = Vec2::from_grid_frac(x, y);
let eps = SCALE / 2.0;
let max_y = self.a.y.max(self.b.y);
(self.a.x - target.x).abs() < eps && (max_y - target.y).abs() < eps
}
pub fn left_ends_at(&self, x: i32, y: i32) -> bool {
if !self.is_horizontal() {
return false;
}
let target = Vec2::from_grid(x, y);
let eps = SCALE / 2.0;
let left_x = target.x - SCALE / 2.0;
((self.a.x - left_x).abs() < eps && (self.a.y - target.y).abs() < eps)
|| ((self.b.x - left_x).abs() < eps && (self.b.y - target.y).abs() < eps)
}
pub fn right_ends_at(&self, x: i32, y: i32) -> bool {
if !self.is_horizontal() {
return false;
}
let target = Vec2::from_grid(x, y);
let eps = SCALE / 2.0;
let right_x = target.x + SCALE / 2.0;
((self.a.x - right_x).abs() < eps && (self.a.y - target.y).abs() < eps)
|| ((self.b.x - right_x).abs() < eps && (self.b.y - target.y).abs() < eps)
}
pub fn vertical_passes_through(&self, x: i32, y: i32) -> bool {
if !self.is_vertical() {
return false;
}
let target = Vec2::from_grid(x, y);
let eps = SCALE / 2.0;
if (self.a.x - target.x).abs() > eps {
return false;
}
let min_y = self.a.y.min(self.b.y);
let max_y = self.a.y.max(self.b.y);
target.y >= min_y - eps && target.y <= max_y + eps
}
pub fn horizontal_passes_through(&self, x: i32, y: i32) -> bool {
if !self.is_horizontal() {
return false;
}
let target = Vec2::from_grid(x, y);
let eps = SCALE / 2.0;
if (self.a.y - target.y).abs() > eps {
return false;
}
let min_x = self.a.x.min(self.b.x);
let max_x = self.a.x.max(self.b.x);
target.x >= min_x - eps && target.x <= max_x + eps
}
pub fn diagonal_up_ends_at(&self, x: i32, y: i32) -> bool {
if !self.is_diagonal() {
return false;
}
let target = Vec2::from_grid(x, y);
let eps = SCALE / 2.0;
let upper = if self.a.y < self.b.y { &self.a } else { &self.b };
(upper.x - target.x).abs() < eps && (upper.y - target.y).abs() < eps
}
pub fn diagonal_down_ends_at(&self, x: i32, y: i32) -> bool {
if !self.is_diagonal() {
return false;
}
let target = Vec2::from_grid(x, y);
let eps = SCALE / 2.0;
let lower = if self.a.y > self.b.y { &self.a } else { &self.b };
(lower.x - target.x).abs() < eps && (lower.y - target.y).abs() < eps
}
pub fn back_diagonal_up_ends_at(&self, x: i32, y: i32) -> bool {
if !self.is_back_diagonal() {
return false;
}
let target = Vec2::from_grid(x, y);
let eps = SCALE / 2.0;
let upper = if self.a.y < self.b.y { &self.a } else { &self.b };
(upper.x - target.x).abs() < eps && (upper.y - target.y).abs() < eps
}
pub fn back_diagonal_down_ends_at(&self, x: i32, y: i32) -> bool {
if !self.is_back_diagonal() {
return false;
}
let target = Vec2::from_grid(x, y);
let eps = SCALE / 2.0;
let lower = if self.a.y > self.b.y { &self.a } else { &self.b };
(lower.x - target.x).abs() < eps && (lower.y - target.y).abs() < eps
}
pub fn to_svg_paths(&self) -> Vec<String> {
if self.style.squiggle && self.is_horizontal() {
return vec![self.squiggle_svg()];
}
if self.style.double {
let vx = self.b.x - self.a.x;
let vy = self.b.y - self.a.y;
let s = (vx * vx + vy * vy).sqrt();
let px = vy / (s * SCALE / ASPECT); let py = -vx / (s * SCALE);
let offset_x = px * SCALE;
let offset_y = py * SCALE * ASPECT;
vec![
self.offset_line_svg(offset_x, offset_y),
self.offset_line_svg(-offset_x, -offset_y),
]
} else {
vec![self.single_line_svg()]
}
}
fn single_line_svg(&self) -> String {
if let (Some(c), Some(d)) = (self.c, self.d) {
format!(
"M {} C {} {} {}",
self.a.coords(),
c.coords(),
d.coords(),
self.b.coords()
)
} else {
format!("M {} L {}", self.a.coords(), self.b.coords())
}
}
fn offset_line_svg(&self, dx: f64, dy: f64) -> String {
let a = self.a.offset_pixels(dx, dy);
let b = self.b.offset_pixels(dx, dy);
if let (Some(c), Some(d)) = (self.c, self.d) {
let c = c.offset_pixels(dx, dy);
let d = d.offset_pixels(dx, dy);
format!(
"M {} C {} {} {}",
a.coords(),
c.coords(),
d.coords(),
b.coords()
)
} else {
format!("M {} L {}", a.coords(), b.coords())
}
}
fn squiggle_svg(&self) -> String {
let y = self.a.y;
let amplitude = SCALE * ASPECT * 0.2;
let mut result = format!("M {},{}", format_coord(self.a.x), format_coord(y));
let grid_x0 = self.a.x / SCALE - 1.0;
let grid_x1 = self.b.x / SCALE - 1.0;
let step = SCALE / 4.0; let mut x = self.a.x; let mut grid_x = grid_x0;
while grid_x < grid_x1 {
let up_x = x + step;
let up_y = y - amplitude;
let mid_x = x + step * 2.0;
let _ = write!(
result,
" Q {},{} {},{}",
format_coord(up_x),
format_coord(up_y),
format_coord(mid_x),
format_coord(y)
);
let down_x = mid_x + step;
let down_y = y + amplitude;
let next_x = mid_x + step * 2.0;
let _ = write!(
result,
" Q {},{} {},{}",
format_coord(down_x),
format_coord(down_y),
format_coord(next_x),
format_coord(y)
);
x = next_x;
grid_x += 1.0;
}
result.push(' ');
result
}
}
#[derive(Debug, Default)]
pub struct PathSet {
paths: Vec<Path>,
}
impl PathSet {
pub fn new() -> Self {
Self { paths: Vec::new() }
}
pub fn insert(&mut self, path: Path) {
if !path.is_degenerate() {
self.paths.push(path);
}
}
pub fn iter(&self) -> impl Iterator<Item = &Path> {
self.paths.iter()
}
pub fn len(&self) -> usize {
self.paths.len()
}
pub fn is_empty(&self) -> bool {
self.paths.is_empty()
}
pub fn up_ends_at(&self, x: i32, y: i32) -> bool {
self.paths.iter().any(|p| p.up_ends_at(x, y))
}
pub fn up_ends_at_frac(&self, x: f64, y: f64) -> bool {
self.paths.iter().any(|p| p.up_ends_at_frac(x, y))
}
pub fn down_ends_at(&self, x: i32, y: i32) -> bool {
self.paths.iter().any(|p| p.down_ends_at(x, y))
}
pub fn down_ends_at_frac(&self, x: f64, y: f64) -> bool {
self.paths.iter().any(|p| p.down_ends_at_frac(x, y))
}
pub fn left_ends_at(&self, x: i32, y: i32) -> bool {
self.paths.iter().any(|p| p.left_ends_at(x, y))
}
pub fn right_ends_at(&self, x: i32, y: i32) -> bool {
self.paths.iter().any(|p| p.right_ends_at(x, y))
}
pub fn vertical_passes_through(&self, x: i32, y: i32) -> bool {
self.paths.iter().any(|p| p.vertical_passes_through(x, y))
}
pub fn horizontal_passes_through(&self, x: i32, y: i32) -> bool {
self.paths.iter().any(|p| p.horizontal_passes_through(x, y))
}
pub fn diagonal_up_ends_at(&self, x: i32, y: i32) -> bool {
self.paths.iter().any(|p| p.diagonal_up_ends_at(x, y))
}
pub fn diagonal_down_ends_at(&self, x: i32, y: i32) -> bool {
self.paths.iter().any(|p| p.diagonal_down_ends_at(x, y))
}
pub fn back_diagonal_up_ends_at(&self, x: i32, y: i32) -> bool {
self.paths.iter().any(|p| p.back_diagonal_up_ends_at(x, y))
}
pub fn back_diagonal_down_ends_at(&self, x: i32, y: i32) -> bool {
self.paths.iter().any(|p| p.back_diagonal_down_ends_at(x, y))
}
pub fn to_svg(&self) -> String {
let mut result = String::new();
for path in &self.paths {
let dash = if path.style.dashed {
" stroke-dasharray=\"4,2\""
} else {
""
};
for path_data in path.to_svg_paths() {
let _ = write!(
result,
"<path d=\"{}\" fill=\"none\" stroke=\"var(--aasvg-stroke)\"{}/>\n",
path_data, dash
);
}
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_vec2_from_grid() {
let v = Vec2::from_grid(0, 0);
assert_eq!(v.x, SCALE);
assert_eq!(v.y, SCALE * ASPECT);
let v = Vec2::from_grid(1, 1);
assert_eq!(v.x, 2.0 * SCALE);
assert_eq!(v.y, 2.0 * SCALE * ASPECT);
}
#[test]
fn test_path_direction() {
let v = Path::line_from_grid(0, 0, 0, 2);
assert!(v.is_vertical());
assert!(!v.is_horizontal());
let h = Path::line_from_grid(0, 0, 2, 0);
assert!(h.is_horizontal());
assert!(!h.is_vertical());
}
#[test]
fn test_path_svg() {
let p = Path::line(Vec2::new(10.0, 20.0), Vec2::new(30.0, 40.0));
assert_eq!(p.to_svg_paths(), vec!["M 10,20 L 30,40"]);
}
}