use crate::{Path2D, PathVerb, Point, ValidationError};
#[cfg(test)]
mod tests;
pub const MAX_PERSPECTIVE_CURVE_PIECES: u32 = 64;
const NEAR_FRACTION: f64 = 1e-3;
const LABEL: &str = "perspective";
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Perspective {
pivot: Point,
rotation: [f64; 3],
distance: f64,
}
impl Perspective {
pub fn new(
pivot: Point,
rotation_x: f64,
rotation_y: f64,
rotation_z: f64,
distance: f64,
) -> Result<Self, ValidationError> {
let finite = [pivot.x, pivot.y, rotation_x, rotation_y, rotation_z]
.iter()
.all(|value| value.is_finite());
if !finite {
return Err(ValidationError::invalid(
LABEL,
"rotation",
"Pivot and angles must be finite",
));
}
if !distance.is_finite() || distance <= 0.0 {
return Err(ValidationError::invalid(
LABEL,
"distance",
"Camera distance must be finite and positive",
));
}
Ok(Self {
pivot,
rotation: [rotation_x, rotation_y, rotation_z],
distance,
})
}
pub fn pivot(&self) -> Point {
self.pivot
}
pub fn rotation(&self) -> [f64; 3] {
self.rotation
}
pub fn distance(&self) -> f64 {
self.distance
}
pub fn faces_camera(&self) -> bool {
libm::cos(self.rotation[0]) * libm::cos(self.rotation[1]) > 0.0
}
pub fn project(&self, point: Point) -> Result<Point, ValidationError> {
let (x, y) = (point.x - self.pivot.x, point.y - self.pivot.y);
let [rotation_x, rotation_y, rotation_z] = self.rotation;
let (sin, cos) = (libm::sin(rotation_z), libm::cos(rotation_z));
let (x, y) = (x * cos - y * sin, x * sin + y * cos);
let (sin, cos) = (libm::sin(rotation_y), libm::cos(rotation_y));
let (x, z) = (x * cos, -x * sin);
let (sin, cos) = (libm::sin(rotation_x), libm::cos(rotation_x));
let (y, z) = (y * cos - z * sin, y * sin + z * cos);
let depth = self.distance - z;
if !depth.is_finite() || depth <= self.distance * NEAR_FRACTION {
return Err(ValidationError::invalid(
LABEL,
"depth",
"Point reaches or passes the camera plane",
));
}
let scale = self.distance / depth;
Ok(Point::new(
self.pivot.x + x * scale,
self.pivot.y + y * scale,
))
}
pub fn project_path(&self, path: &Path2D, pieces: u32) -> Result<Path2D, ValidationError> {
if !(1..=MAX_PERSPECTIVE_CURVE_PIECES).contains(&pieces) {
return Err(ValidationError::limit(
LABEL,
"pieces",
"Curve pieces must be between 1 and 64",
));
}
let mut result = Path2D::new();
let mut current = Point::ORIGIN;
let mut start = Point::ORIGIN;
for verb in path.verbs() {
match *verb {
PathVerb::MoveTo(point) => {
result.try_move_to(self.project(point)?)?;
current = point;
start = point;
}
PathVerb::LineTo(point) => {
result.try_line_to(self.project(point)?)?;
current = point;
}
PathVerb::QuadraticTo(control, end) => {
let first = lerp(current, control, 2.0 / 3.0);
let second = lerp(end, control, 2.0 / 3.0);
self.cubic(&mut result, [current, first, second, end], pieces)?;
current = end;
}
PathVerb::CubicTo(first, second, end) => {
self.cubic(&mut result, [current, first, second, end], pieces)?;
current = end;
}
PathVerb::Close => {
result.try_close()?;
current = start;
}
}
}
Ok(result)
}
fn cubic(
&self,
result: &mut Path2D,
curve: [Point; 4],
pieces: u32,
) -> Result<(), ValidationError> {
let curve = kurbo::CubicBez::new(
(curve[0].x, curve[0].y),
(curve[1].x, curve[1].y),
(curve[2].x, curve[2].y),
(curve[3].x, curve[3].y),
);
for index in 0..pieces {
let piece = kurbo::ParamCurve::subsegment(
&curve,
f64::from(index) / f64::from(pieces)..f64::from(index + 1) / f64::from(pieces),
);
let [first, second, end] = [piece.p1, piece.p2, piece.p3];
result.try_cubic_to(
self.project(Point::new(first.x, first.y))?,
self.project(Point::new(second.x, second.y))?,
self.project(Point::new(end.x, end.y))?,
)?;
}
Ok(())
}
}
fn lerp(from: Point, to: Point, amount: f64) -> Point {
Point::new(
from.x + (to.x - from.x) * amount,
from.y + (to.y - from.y) * amount,
)
}