1use crate::{Path2D, PathVerb, Point, ValidationError};
4
5#[cfg(test)]
6mod tests;
7
8pub const MAX_PERSPECTIVE_CURVE_PIECES: u32 = 64;
10
11const NEAR_FRACTION: f64 = 1e-3;
13
14const LABEL: &str = "perspective";
15
16#[derive(Clone, Copy, Debug, PartialEq)]
40pub struct Perspective {
41 pivot: Point,
42 rotation: [f64; 3],
43 distance: f64,
44}
45
46impl Perspective {
47 pub fn new(
49 pivot: Point,
50 rotation_x: f64,
51 rotation_y: f64,
52 rotation_z: f64,
53 distance: f64,
54 ) -> Result<Self, ValidationError> {
55 let finite = [pivot.x, pivot.y, rotation_x, rotation_y, rotation_z]
56 .iter()
57 .all(|value| value.is_finite());
58 if !finite {
59 return Err(ValidationError::invalid(
60 LABEL,
61 "rotation",
62 "Pivot and angles must be finite",
63 ));
64 }
65 if !distance.is_finite() || distance <= 0.0 {
66 return Err(ValidationError::invalid(
67 LABEL,
68 "distance",
69 "Camera distance must be finite and positive",
70 ));
71 }
72 Ok(Self {
73 pivot,
74 rotation: [rotation_x, rotation_y, rotation_z],
75 distance,
76 })
77 }
78
79 pub fn pivot(&self) -> Point {
81 self.pivot
82 }
83
84 pub fn rotation(&self) -> [f64; 3] {
86 self.rotation
87 }
88
89 pub fn distance(&self) -> f64 {
91 self.distance
92 }
93
94 pub fn faces_camera(&self) -> bool {
96 libm::cos(self.rotation[0]) * libm::cos(self.rotation[1]) > 0.0
97 }
98
99 pub fn project(&self, point: Point) -> Result<Point, ValidationError> {
101 let (x, y) = (point.x - self.pivot.x, point.y - self.pivot.y);
102 let [rotation_x, rotation_y, rotation_z] = self.rotation;
103 let (sin, cos) = (libm::sin(rotation_z), libm::cos(rotation_z));
104 let (x, y) = (x * cos - y * sin, x * sin + y * cos);
105 let (sin, cos) = (libm::sin(rotation_y), libm::cos(rotation_y));
106 let (x, z) = (x * cos, -x * sin);
107 let (sin, cos) = (libm::sin(rotation_x), libm::cos(rotation_x));
108 let (y, z) = (y * cos - z * sin, y * sin + z * cos);
109 let depth = self.distance - z;
110 if !depth.is_finite() || depth <= self.distance * NEAR_FRACTION {
111 return Err(ValidationError::invalid(
112 LABEL,
113 "depth",
114 "Point reaches or passes the camera plane",
115 ));
116 }
117 let scale = self.distance / depth;
118 Ok(Point::new(
119 self.pivot.x + x * scale,
120 self.pivot.y + y * scale,
121 ))
122 }
123
124 pub fn project_path(&self, path: &Path2D, pieces: u32) -> Result<Path2D, ValidationError> {
131 if !(1..=MAX_PERSPECTIVE_CURVE_PIECES).contains(&pieces) {
132 return Err(ValidationError::limit(
133 LABEL,
134 "pieces",
135 "Curve pieces must be between 1 and 64",
136 ));
137 }
138 let mut result = Path2D::new();
139 let mut current = Point::ORIGIN;
140 let mut start = Point::ORIGIN;
141 for verb in path.verbs() {
142 match *verb {
143 PathVerb::MoveTo(point) => {
144 result.try_move_to(self.project(point)?)?;
145 current = point;
146 start = point;
147 }
148 PathVerb::LineTo(point) => {
149 result.try_line_to(self.project(point)?)?;
150 current = point;
151 }
152 PathVerb::QuadraticTo(control, end) => {
153 let first = lerp(current, control, 2.0 / 3.0);
154 let second = lerp(end, control, 2.0 / 3.0);
155 self.cubic(&mut result, [current, first, second, end], pieces)?;
156 current = end;
157 }
158 PathVerb::CubicTo(first, second, end) => {
159 self.cubic(&mut result, [current, first, second, end], pieces)?;
160 current = end;
161 }
162 PathVerb::Close => {
163 result.try_close()?;
164 current = start;
165 }
166 }
167 }
168 Ok(result)
169 }
170
171 fn cubic(
172 &self,
173 result: &mut Path2D,
174 curve: [Point; 4],
175 pieces: u32,
176 ) -> Result<(), ValidationError> {
177 let curve = kurbo::CubicBez::new(
178 (curve[0].x, curve[0].y),
179 (curve[1].x, curve[1].y),
180 (curve[2].x, curve[2].y),
181 (curve[3].x, curve[3].y),
182 );
183 for index in 0..pieces {
184 let piece = kurbo::ParamCurve::subsegment(
185 &curve,
186 f64::from(index) / f64::from(pieces)..f64::from(index + 1) / f64::from(pieces),
187 );
188 let [first, second, end] = [piece.p1, piece.p2, piece.p3];
189 result.try_cubic_to(
190 self.project(Point::new(first.x, first.y))?,
191 self.project(Point::new(second.x, second.y))?,
192 self.project(Point::new(end.x, end.y))?,
193 )?;
194 }
195 Ok(())
196 }
197}
198
199fn lerp(from: Point, to: Point, amount: f64) -> Point {
200 Point::new(
201 from.x + (to.x - from.x) * amount,
202 from.y + (to.y - from.y) * amount,
203 )
204}