use itertools::multizip;
use std::{
f64::consts::PI,
hash::{Hash, Hasher},
ops,
};
use crate::{Color, Error::*, Result};
const PRECISION: i32 = 6;
pub trait Polygon {
fn points(&self, margin: f64) -> Result<Vec<Point>>;
fn render(&self, context: &cairo::Context, margin: f64) -> Result<()>;
}
#[derive(Clone, Copy, Debug)]
pub struct Shape {
sides: i32,
point: Point,
rotation: f64,
fill: Color,
stroke: Color,
}
impl Shape {
pub fn new(sides: i32, fill: Color, stroke: Color) -> Result<Shape> {
if sides < 3 {
return Err(InvalidShape);
}
Ok(Shape {
sides,
point: Point::origin(),
rotation: 0.0,
fill,
stroke,
})
}
pub fn sides(&self) -> i32 {
self.sides
}
pub fn point(&self) -> Point {
self.point
}
pub fn rotation(&self) -> f64 {
self.rotation
}
pub fn fill(&self) -> Color {
self.fill
}
pub fn stroke(&self) -> Color {
self.stroke
}
fn edge(&self, index: usize, margin: f64) -> Result<Edge> {
let es = self.edges(margin)?;
es.get(index)
.ok_or(OutOfBounds {
index: index,
length: es.len(),
name: String::from("shape edges"),
})
.map(|(p0, p1)| (p0.clone(), p1.clone()))
}
fn edges(&self, margin: f64) -> Result<Vec<Edge>> {
let ps = self.points(margin)?;
let mut es = Vec::new();
for i in 0..self.sides {
let i = i as usize;
let p0 = ps.get(i).ok_or(OutOfBounds {
index: i,
length: ps.len(),
name: String::from("shape vertices"),
})?;
let p1 = ps.get(i + 1).ok_or(OutOfBounds {
index: i + 1,
length: ps.len(),
name: String::from("shape vertices"),
})?;
es.push((p0.clone(), p1.clone()));
}
Ok(es)
}
pub fn adjacent(&self, sides: i32, edge: usize, fill: Color, stroke: Color) -> Result<Shape> {
let (p0, p1) = self.edge(edge, 0.0)?;
let angle = 2.0 * PI / sides as f64;
let a = (p1.y - p0.y).atan2(p1.x - p0.x);
let b = a - PI / 2.0;
let d = 0.5 / (angle / 2.0).tan();
let p = Point {
x: p0.x + (p1.x - p0.x) / 2.0 + b.cos() * d,
y: p0.y + (p1.y - p0.y) / 2.0 + b.sin() * d,
};
let r = a + angle * ((sides - 1) as f64 / 2.0);
Ok(Shape {
sides: sides,
point: p,
rotation: r,
fill: fill,
stroke: stroke,
})
}
pub fn render_edge_labels(&self, context: &cairo::Context, margin: f64) -> Result<()> {
let es = self.edges(margin)?;
for (i, e) in es.iter().enumerate() {
let text = i.to_string();
let (p0, p1) = e;
let te = context.text_extents(&text)?;
let x = p0.x + (p1.x - p0.x) / 2.0 - te.width / 2.0;
let y = p0.y + (p1.y - p0.y) / 2.0 - te.height / 2.0;
context.set_source_rgb(0.0, 0.0, 0.0);
context.move_to(x, y);
context.show_text(&text)?;
}
Ok(())
}
pub fn render_label(&self, context: &cairo::Context, text: &str) -> Result<()> {
let te = context.text_extents(text)?;
let x = self.point.x - te.width / 2.0;
let y = self.point.y - te.height / 2.0;
context.set_source_rgb(0.0, 0.0, 0.0);
context.move_to(x, y);
context.show_text(text)?;
Ok(())
}
pub fn clone_at(&self, point: Point) -> Shape {
let mut s = self.clone();
s.point = point;
s
}
}
impl Polygon for Shape {
fn points(&self, margin: f64) -> Result<Vec<Point>> {
let angle = 2.0 * PI / self.sides as f64;
let rotation = self.rotation - PI / 2.0;
let angles = (0..=self.sides)
.map(|i| (i % self.sides) as f64 * angle + rotation)
.collect::<Vec<f64>>();
let d = {
let a = angle / 2.0;
0.5 / a.sin() - margin / a.cos()
};
let points = angles
.iter()
.map(|a| Point {
x: self.point.x + a.cos() * d,
y: self.point.y + a.sin() * d,
})
.collect();
Ok(points)
}
fn render(&self, context: &cairo::Context, margin: f64) -> Result<()> {
render(context, self.points(margin)?, self.fill, self.stroke)
}
}
#[derive(Clone, Debug)]
pub struct Dual {
points: Vec<Point>,
fill: Color,
stroke: Color,
}
impl Dual {
pub fn new(points: Vec<Point>, fill: Color, stroke: Color) -> Dual {
Dual {
points,
fill,
stroke,
}
}
fn inset_polygon(points: Vec<Point>, margin: f64) -> Result<Vec<Point>> {
let p = points.get(points.len() - 2).ok_or(OutOfBounds {
index: points.len() - 2,
length: points.len(),
name: String::from("shape points"),
})?;
let mut points = points.clone();
points.insert(0, *p);
let mut rs = multizip((&points, &points[1..], &points[2..]))
.map(|(p0, p1, p2)| Dual::inset_corner((p0.clone(), p1.clone(), p2.clone()), margin))
.collect::<Vec<Point>>();
rs.push(rs[0]);
Ok(rs)
}
fn inset_corner(plane: Plane, margin: f64) -> Point {
let (p0, p1, p2) = plane;
let a0 = (p1.y - p0.y).atan2(p1.x - p0.x) - PI / 2.0;
let a1 = (p2.y - p1.y).atan2(p2.x - p1.x) - PI / 2.0;
let (ax0, ay0) = (p0.x + a0.cos() * margin, p0.y + a0.sin() * margin);
let (ax1, ay1) = (p1.x + a0.cos() * margin, p1.y + a0.sin() * margin);
let (bx0, by0) = (p1.x + a1.cos() * margin, p1.y + a1.sin() * margin);
let (bx1, by1) = (p2.x + a1.cos() * margin, p2.y + a1.sin() * margin);
let (ady, adx) = (ay1 - ay0, ax0 - ax1);
let (bdy, bdx) = (by1 - by0, bx0 - bx1);
let c0 = ady * ax0 + adx * ay0;
let c1 = bdy * bx0 + bdx * by0;
let d = ady * bdx - bdy * adx;
Point {
x: (bdx * c0 - adx * c1) / d,
y: (ady * c1 - bdy * c0) / d,
}
}
}
impl Polygon for Dual {
fn points(&self, margin: f64) -> Result<Vec<Point>> {
if margin == 0.0 {
Ok(self.points.clone())
} else {
Dual::inset_polygon(self.points.clone(), margin)
}
}
fn render(&self, context: &cairo::Context, margin: f64) -> Result<()> {
render(context, self.points(margin)?, self.fill, self.stroke)
}
}
#[derive(Clone, Copy, Debug)]
pub struct Point {
pub x: f64,
pub y: f64,
}
impl Point {
pub fn origin() -> Point {
Point { x: 0.0, y: 0.0 }
}
fn normalize(n: f64) -> i32 {
(n * 10_f64.powi(PRECISION)).round() as i32
}
}
impl PartialEq for Point {
fn eq(&self, other: &Self) -> bool {
Point::normalize(self.x) == Point::normalize(other.x)
&& Point::normalize(self.y) == Point::normalize(other.y)
}
}
impl Eq for Point {}
impl Hash for Point {
fn hash<H: Hasher>(&self, state: &mut H) {
Point::normalize(self.x).hash(state);
Point::normalize(self.y).hash(state);
}
}
impl ops::Add<Point> for Point {
type Output = Point;
fn add(self, _rhs: Point) -> Point {
Point {
x: self.x + _rhs.x,
y: self.y + _rhs.y,
}
}
}
type Edge = (Point, Point);
type Plane = (Point, Point, Point);
fn render(context: &cairo::Context, points: Vec<Point>, fill: Color, stroke: Color) -> Result<()> {
for i in 0..points.len() {
let p = points[i];
match i {
0 => context.move_to(p.x, p.y),
_ => context.line_to(p.x, p.y),
}
}
let (r, g, b) = fill.rgb_unit_int();
context.set_source_rgb(r, g, b);
context.fill_preserve()?;
let (r, g, b) = stroke.rgb_unit_int();
context.set_source_rgb(r, g, b);
context.stroke()?;
Ok(())
}