rust_motion_paths/
bounding_box.rs1use crate::parser::parse_path;
4use crate::point_at_length::get_point_at_length;
5use crate::types::{BoundingBox, Instruction, Point};
6
7#[derive(Debug)]
8struct BoundsTracker {
9 min_x: f64,
10 min_y: f64,
11 max_x: f64,
12 max_y: f64,
13 has_points: bool,
14}
15
16impl BoundsTracker {
17 fn new() -> Self {
18 Self {
19 min_x: f64::INFINITY,
20 min_y: f64::INFINITY,
21 max_x: f64::NEG_INFINITY,
22 max_y: f64::NEG_INFINITY,
23 has_points: false,
24 }
25 }
26
27 fn update(&mut self, x: f64, y: f64) {
28 if x.is_finite() && y.is_finite() {
29 self.min_x = self.min_x.min(x);
30 self.min_y = self.min_y.min(y);
31 self.max_x = self.max_x.max(x);
32 self.max_y = self.max_y.max(y);
33 self.has_points = true;
34 }
35 }
36
37 fn update_x(&mut self, x: f64) {
38 if x.is_finite() {
39 self.min_x = self.min_x.min(x);
40 self.max_x = self.max_x.max(x);
41 self.has_points = true;
42 }
43 }
44
45 fn update_y(&mut self, y: f64) {
46 if y.is_finite() {
47 self.min_y = self.min_y.min(y);
48 self.max_y = self.max_y.max(y);
49 self.has_points = true;
50 }
51 }
52
53 fn into_bounding_box(self) -> Option<BoundingBox> {
54 if !self.has_points {
55 None
56 } else {
57 Some(BoundingBox::new(
58 self.min_x,
59 self.min_y,
60 (self.max_x - self.min_x).max(0.0),
61 (self.max_y - self.min_y).max(0.0),
62 ))
63 }
64 }
65}
66
67pub fn get_bounding_box(path: &str) -> Option<BoundingBox> {
71 let instructions = parse_path(path).ok()?;
72 get_instructions_bounding_box(&instructions)
73}
74
75pub fn get_instructions_bounding_box(instructions: &[Instruction]) -> Option<BoundingBox> {
77 if instructions.is_empty() {
78 return None;
79 }
80
81 let mut tracker = BoundsTracker::new();
82 let mut curr = Point::new(0.0, 0.0);
83 let mut start_point = Point::new(0.0, 0.0);
84
85 for inst in instructions {
86 match inst {
87 Instruction::MoveTo { x, y } => {
88 tracker.update(*x, *y);
89 curr = Point::new(*x, *y);
90 start_point = curr;
91 }
92 Instruction::LineTo { x, y } => {
93 tracker.update(*x, *y);
94 curr = Point::new(*x, *y);
95 }
96 Instruction::ClosePath => {
97 curr = start_point;
98 }
99 Instruction::QuadCurveTo { x1, y1, x, y } => {
100 tracker.update(curr.x, curr.y);
101 tracker.update(*x, *y);
102
103 for (p0, p1, p2, is_x) in [(curr.x, *x1, *x, true), (curr.y, *y1, *y, false)] {
106 let denom = p0 - 2.0 * p1 + p2;
107 if denom.abs() > 1e-9 {
108 let t = (p0 - p1) / denom;
109 if (0.0..=1.0).contains(&t) {
110 let val =
111 (1.0 - t) * (1.0 - t) * p0 + 2.0 * (1.0 - t) * t * p1 + t * t * p2;
112 if is_x {
113 tracker.update_x(val);
114 } else {
115 tracker.update_y(val);
116 }
117 }
118 }
119 }
120
121 curr = Point::new(*x, *y);
122 }
123 Instruction::CubicCurveTo {
124 x1,
125 y1,
126 x2,
127 y2,
128 x,
129 y,
130 } => {
131 tracker.update(curr.x, curr.y);
132 tracker.update(*x, *y);
133
134 for (p0, p1, p2, p3, is_x) in
136 [(curr.x, *x1, *x2, *x, true), (curr.y, *y1, *y2, *y, false)]
137 {
138 let a = -p0 + 3.0 * p1 - 3.0 * p2 + p3;
139 let b = 2.0 * (p0 - 2.0 * p1 + p2);
140 let c = p1 - p0;
141
142 let evaluate_t = |t: f64| -> f64 {
143 let omt = 1.0 - t;
144 omt * omt * omt * p0
145 + 3.0 * omt * omt * t * p1
146 + 3.0 * omt * t * t * p2
147 + t * t * t * p3
148 };
149
150 if a.abs() < 1e-9 {
151 if b.abs() > 1e-9 {
152 let t = -c / b;
153 if (0.0..=1.0).contains(&t) {
154 let val = evaluate_t(t);
155 if is_x {
156 tracker.update_x(val);
157 } else {
158 tracker.update_y(val);
159 }
160 }
161 }
162 } else {
163 let disc = b * b - 4.0 * a * c;
164 if disc >= 0.0 {
165 let sqrt_disc = disc.sqrt();
166 let t1 = (-b - sqrt_disc) / (2.0 * a);
167 let t2 = (-b + sqrt_disc) / (2.0 * a);
168 if (0.0..=1.0).contains(&t1) {
169 let val = evaluate_t(t1);
170 if is_x {
171 tracker.update_x(val);
172 } else {
173 tracker.update_y(val);
174 }
175 }
176 if (0.0..=1.0).contains(&t2) {
177 let val = evaluate_t(t2);
178 if is_x {
179 tracker.update_x(val);
180 } else {
181 tracker.update_y(val);
182 }
183 }
184 }
185 }
186 }
187
188 curr = Point::new(*x, *y);
189 }
190 Instruction::ArcTo { x, y, .. } => {
191 tracker.update(curr.x, curr.y);
192 tracker.update(*x, *y);
193 let single_arc_insts = vec![
195 Instruction::MoveTo {
196 x: curr.x,
197 y: curr.y,
198 },
199 inst.clone(),
200 ];
201 for step in 1..8 {
202 let progress = step as f64 / 8.0;
203 let pt = get_point_at_length(
204 &crate::parser::serialize_instructions(&single_arc_insts),
205 progress * crate::length::get_instructions_length(&single_arc_insts),
206 );
207 tracker.update(pt.x, pt.y);
208 }
209 curr = Point::new(*x, *y);
210 }
211 }
212 }
213
214 tracker.into_bounding_box()
215}
216
217#[cfg(test)]
218mod tests {
219 use super::*;
220
221 #[test]
222 fn test_rect_bounding_box() {
223 let path = "M 10 20 L 110 20 L 110 70 L 10 70 Z";
224 let bbox = get_bounding_box(path).expect("bbox should exist");
225 assert!((bbox.x - 10.0).abs() < 1e-5);
226 assert!((bbox.y - 20.0).abs() < 1e-5);
227 assert!((bbox.width - 100.0).abs() < 1e-5);
228 assert!((bbox.height - 50.0).abs() < 1e-5);
229 }
230
231 #[test]
232 fn test_cubic_curve_bounding_box() {
233 let path = "M 0 0 C 50 100 150 100 200 0";
235 let bbox = get_bounding_box(path).expect("bbox should exist");
236 assert!((bbox.x - 0.0).abs() < 1e-5);
237 assert!((bbox.width - 200.0).abs() < 1e-5);
238 assert!(bbox.height > 50.0); }
240}