1use crate::{
5 Affine, Arc, BezPath, CubicBez, Join, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point,
6 QuadBez, Shape, Vec2, bezpath::close_subpaths, segments,
7};
8
9#[cfg(not(feature = "std"))]
10use crate::common::FloatFuncs;
11
12#[derive(Clone, Copy, Debug, PartialEq)]
14#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct Diagonal2 {
17 pub xx: f64,
19 pub yy: f64,
21}
22
23struct ExpandCtx {
24 expand: Diagonal2,
25 join: Join,
26 miter_limit: f64,
27 tolerance: f64,
28 result: BezPath,
29 first_n: Option<Vec2>,
30 first_tan: Vec2,
31 last_pt: Point,
32 last_n: Option<Vec2>,
33 last_tan: Vec2,
34}
35
36struct CubicCtx {
37 q: QuadBez,
38 utan0: Vec2,
39 utan1: Vec2,
40}
41
42struct TwoPointSample {
43 a_n: f64,
44 b_n: f64,
45 c_n: f64,
46}
47
48impl Diagonal2 {
49 pub const fn new(xx: f64, yy: f64) -> Self {
51 Self { xx, yy }
52 }
53
54 pub const fn inv(self) -> Self {
58 Diagonal2::new(1.0 / self.xx, 1.0 / self.yy)
59 }
60
61 pub const fn abs(self) -> Self {
63 Self::new(self.xx.abs(), self.yy.abs())
64 }
65
66 pub fn scale_normal(self, n: Vec2) -> Vec2 {
74 let z = self * n;
75 let z_hypot2 = z.hypot2();
76 if z_hypot2 == 0.0 {
77 Vec2::ZERO
78 } else {
79 let inv_scale = 1.0 / z_hypot2.sqrt();
80 self.abs() * z * inv_scale
81 }
82 }
83}
84
85impl core::ops::Neg for Diagonal2 {
86 type Output = Self;
87
88 fn neg(self) -> Self {
89 Diagonal2::new(-self.xx, -self.yy)
90 }
91}
92
93impl core::ops::Mul<Vec2> for Diagonal2 {
94 type Output = Vec2;
95
96 fn mul(self, rhs: Vec2) -> Vec2 {
97 Vec2::new(self.xx * rhs.x, self.yy * rhs.y)
98 }
99}
100
101impl core::ops::Mul<Point> for Diagonal2 {
102 type Output = Point;
103
104 fn mul(self, rhs: Point) -> Point {
105 Point::new(self.xx * rhs.x, self.yy * rhs.y)
106 }
107}
108
109pub fn expand_path(
128 path: impl Shape,
129 mut expand: Diagonal2,
130 join: Join,
131 miter_limit: f64,
132 tolerance: f64,
133) -> BezPath {
134 if segments(close_subpaths(path.path_elements(tolerance))).area() >= 0.0 {
135 expand = -expand;
136 }
137 expand_path_signed(path, expand, join, miter_limit, tolerance)
138}
139
140pub fn expand_path_signed(
147 path: impl Shape,
148 expand: Diagonal2,
149 join: Join,
150 miter_limit: f64,
151 tolerance: f64,
152) -> BezPath {
153 let result = BezPath::new();
154 let mut ctx = ExpandCtx {
155 expand,
156 join,
157 miter_limit,
158 tolerance,
159 result,
160 first_n: None,
161 first_tan: Vec2::default(),
162 last_pt: Point::default(),
163 last_n: None,
164 last_tan: Vec2::default(),
165 };
166 let mut first_pt = Point::default();
167 for el in path.path_elements(tolerance) {
168 match el {
169 PathEl::MoveTo(point) => {
170 ctx.do_close_path(first_pt);
171 first_pt = point;
172 ctx.last_pt = point;
173 }
174 PathEl::LineTo(p1) => ctx.do_line(p1),
175 PathEl::QuadTo(p1, p2) => ctx.do_quad(p1, p2),
176 PathEl::CurveTo(p1, p2, p3) => ctx.do_cubic(p1, p2, p3),
177 PathEl::ClosePath => ctx.do_close_path(first_pt),
178 }
179 }
180 ctx.do_close_path(first_pt);
182 ctx.result
183}
184
185impl ExpandCtx {
186 fn in_tolerance(&self, v: Vec2) -> bool {
188 v.hypot2() < self.tolerance * self.tolerance
189 }
190
191 fn do_line(&mut self, p1: Point) {
193 if p1 == self.last_pt {
194 return;
195 }
196 let tan = p1 - self.last_pt;
197 let n = self.expand.scale_normal(tan.turn_90());
198 self.do_join(n, tan, true);
199 let out_p1 = p1 + n;
200 self.result.line_to(out_p1);
201 self.last_n = Some(n);
202 self.last_tan = tan;
203 self.last_pt = p1;
204 }
205
206 fn do_quad(&mut self, p1: Point, p2: Point) {
208 let q0 = p1 - self.last_pt;
209 let q1 = p2 - p1;
210 if self.in_tolerance(q0) || self.in_tolerance(q1) {
211 self.do_line(p2);
212 return;
213 }
214 let einv = self.expand.inv();
215 let utan0 = (einv * q0).normalize();
216 let utan1 = (einv * q1).normalize();
217 let det = utan1.cross(utan0);
218 if det.abs() < 1e-10 {
219 self.do_line(p2);
220 return;
221 }
222 let utanm = (einv * (p2 - self.last_pt)).normalize();
223 let n0 = self.expand.abs() * utan0.turn_90();
224 let n1 = self.expand.abs() * utan1.turn_90();
225 let mid_chord = self.last_pt.midpoint(p2);
226 let m = mid_chord.midpoint(p1) + self.expand.abs() * utanm.turn_90();
227 let out_p0 = self.last_pt + n0;
228 let out_p3 = p2 + n1;
229 let rhs = einv * (m - out_p0.midpoint(out_p3));
230 let idet = (8. / 3.) / det;
231 let a = (utan1.cross(rhs) * idet).max(0.0);
232 let b = (utan0.cross(rhs) * idet).max(0.0);
233 self.do_join(n0, q0, false);
234 let out_p1 = out_p0 + self.expand * (a * utan0);
235 let out_p2 = out_p3 - self.expand * (b * utan1);
236 self.result.curve_to(out_p1, out_p2, out_p3);
237 self.last_n = Some(n1);
238 self.last_tan = q1;
239 self.last_pt = p2;
240 }
241
242 fn do_cubic(&mut self, p1: Point, p2: Point, p3: Point) {
244 let einv = self.expand.inv();
245 let c = CubicBez::new(einv * self.last_pt, einv * p1, einv * p2, einv * p3);
246 let (tan0, tan1) = PathSeg::Cubic(c).tangents();
247 let utan0 = tan0.normalize();
248 let utan1 = tan1.normalize();
249 let q = c.deriv();
250 let p1xp0 = q.p1.to_vec2().cross(q.p0.to_vec2());
251 let p2xp1 = q.p2.to_vec2().cross(q.p1.to_vec2());
252 let cx = CubicCtx { q, utan0, utan1 };
253 let mut soln = if p1xp0 * p2xp1 >= 0.0 {
255 try_one_point(&cx)
256 } else {
257 None
258 };
259 if soln.is_none() {
260 soln = two_point_linear(&cx);
265 }
266 if let Some((a, b)) = soln {
267 let n0 = self.expand.abs() * utan0.turn_90();
268 let n1 = self.expand.abs() * utan1.turn_90();
269 self.do_join(n0, self.expand * utan0, false);
270 let out_p3 = p3 + n1;
271 let out_p1 = p1 + n0 + self.expand.abs() * (a * utan0);
273 let out_p2 = p2 + n1 + self.expand.abs() * (b * utan1);
274 self.result.curve_to(out_p1, out_p2, out_p3);
275 self.last_n = Some(n1);
276 self.last_tan = self.expand * utan1;
277 self.last_pt = p3;
278 } else {
279 self.do_line(p3);
280 }
281 }
282
283 fn do_join(&mut self, n: Vec2, tan: Vec2, is_line: bool) {
288 if let Some(last_n) = self.last_n {
290 let p = self.last_pt + n;
291 if !self.in_tolerance(n - last_n) {
292 if self.join != Join::Bevel {
293 let cross = self.last_tan.cross(tan);
294 if cross * self.expand.xx < 0.0 {
295 match self.join {
296 Join::Bevel => unreachable!(),
297 Join::Miter => {
298 let dot = self.last_tan.dot(tan);
299 let hypot = cross.hypot(dot);
300 if 2.0 * hypot < (hypot + dot) * self.miter_limit * self.miter_limit
301 {
302 let h = (n - last_n).cross(tan) / self.last_tan.cross(tan);
303 let miter_pt = self.last_pt + last_n + h * self.last_tan;
304 if let Some(PathEl::LineTo(p)) =
306 self.result.elements_mut().last_mut()
307 {
308 *p = miter_pt;
309 } else {
310 self.result.line_to(miter_pt);
311 }
312 if is_line {
313 return;
314 }
315 }
316 }
317 Join::Round => {
318 let einv = Diagonal2::new(self.expand.yy, self.expand.xx);
320 let last_tann = einv * self.last_tan;
321 let tann = einv * tan;
322 let crossn = (last_tann).cross(tann);
323 let dotn = (last_tann).dot(tann);
324 let angle = crossn.atan2(dotn).abs();
325 let nt = self.expand * tann.normalize();
326 let a = Affine::new([
327 n.x,
328 n.y,
329 nt.x,
330 nt.y,
331 self.last_pt.x,
332 self.last_pt.y,
333 ]);
334 let arc: Arc =
335 Arc::new(Point::ORIGIN, (1.0, 1.0), -angle, angle, 0.0);
336 let tolerance = self.tolerance / einv.xx.abs().max(einv.yy.abs());
337 arc.to_cubic_beziers(tolerance, |p1, p2, p3| {
338 self.result.curve_to(a * p1, a * p2, a * p3);
339 });
340 return;
341 }
342 }
343 }
344 }
345 self.result.line_to(p);
347 }
348 } else {
349 self.result.move_to(self.last_pt + n);
350 self.first_n = Some(n);
351 self.first_tan = tan;
352 }
353 }
354
355 fn do_close_path(&mut self, first_pt: Point) {
357 if let Some(first_n) = self.first_n.take() {
358 if first_pt.distance_squared(self.last_pt) > self.tolerance * self.tolerance {
360 self.do_line(first_pt);
361 }
362 self.do_join(first_n, self.first_tan, true);
363 self.result.close_path();
364 }
365 self.last_n = None;
366 }
367}
368
369fn try_one_point(cx: &CubicCtx) -> Option<(f64, f64)> {
378 let tan = cx.q.eval(0.5).to_vec2();
380 let tan_hypot2 = tan.hypot2();
381 if tan_hypot2 < 1e-12 {
382 return None;
383 }
384 let utan = tan / tan_hypot2.sqrt();
385 let z = (utan - 0.5 * (cx.utan0 + cx.utan1)).turn_90();
386 let cross = cx.utan0.cross(cx.utan1);
387 if cross.abs() < 1e-12 {
388 return None;
389 }
390 let idet = (8. / 3.) / cross;
391 let a = z.cross(cx.utan1) * idet;
392 let b = cx.utan0.cross(z) * idet;
393 Some((a, b))
397}
398
399fn two_point_linear(cx: &CubicCtx) -> Option<(f64, f64)> {
400 const T0: f64 = 0.35;
401 let s = [T0, 1.0 - T0].map(|t| {
402 let utan = cx.q.eval(t).to_vec2().normalize();
403 let n = utan.turn_90();
404 let utan0_n = cx.utan0.dot(n);
405 let utan1_n = cx.utan1.dot(n);
406 let utan0xn = cx.utan0.dot(utan);
407 let utan1xn = cx.utan1.dot(utan);
408 let mt = 1.0 - t;
409 let b0 = mt * mt * mt;
410 let b1 = 3.0 * mt * t * mt;
411 let b2 = 3.0 * mt * t * t;
412 let b3 = t * t * t;
413 let a_n = b1 * utan0_n;
414 let b_n = b2 * utan1_n;
415 let c_n = (b0 + b1) * utan0xn + (b2 + b3) * utan1xn - 1.0;
416 TwoPointSample { a_n, b_n, c_n }
417 });
418 let det = s[0].a_n * s[1].b_n - s[1].a_n * s[0].b_n;
419 if det.abs().partial_cmp(&1e-12) != Some(core::cmp::Ordering::Greater) {
421 return None;
422 }
423 let idet = -1.0 / det;
424 let a = idet * (s[0].c_n * s[1].b_n - s[1].c_n * s[0].b_n);
425 let b = idet * (s[0].a_n * s[1].c_n - s[1].a_n * s[0].c_n);
426 Some((a, b))
427}
428
429#[cfg(test)]
430mod tests {
431 use std::f64::consts::PI;
432
433 use crate::{
434 Affine, BezPath, Circle, Diagonal2, Join, ParamCurveMoments, PathEl, Point, Rect, Shape,
435 Size, Vec2, expand_path,
436 };
437
438 #[test]
439 fn expand_circle_basic() {
440 const CENTER: Point = Point::new(100., 100.);
441 const RADIUS: f64 = 10.0;
442 let circle = Circle::new(CENTER, RADIUS);
443 let expected_area = PI * RADIUS.powi(2);
444 let expected_perimeter = 2.0 * PI * RADIUS;
445 assert!((circle.area() - expected_area).abs() < 1e-9);
446 assert!((circle.perimeter(1e-12) - expected_perimeter).abs() < 1e-9);
447 const R_DELTA: f64 = 2.0;
448 const EXPAND: Diagonal2 = Diagonal2::new(R_DELTA, R_DELTA);
449 let expanded = expand_path(circle, EXPAND, Join::Bevel, 4.0, 1e-3);
450 let expected_area_expanded = PI * (RADIUS + R_DELTA).powi(2);
451 let expected_perimeter_expanded = 2.0 * PI * (RADIUS + R_DELTA);
452 assert!((expanded.area() - expected_area_expanded).abs() < 4e-2);
453 assert!((expanded.perimeter(1e-12) - expected_perimeter_expanded).abs() < 3e-3);
454
455 let mirror = Affine::reflect(CENTER, Vec2::new(0.0, 1.0));
456 let circle_mirror = mirror * circle.to_path(1e-3);
458 let expanded_mirror = expand_path(circle_mirror, EXPAND, Join::Bevel, 4.0, 1e-3);
459 assert!((-expanded_mirror.area() - expected_area_expanded).abs() < 4e-2);
460 assert!((expanded_mirror.perimeter(1e-12) - expected_perimeter_expanded).abs() < 3e-3);
461 }
462
463 #[test]
464 fn expand_ellipse_anisotropic() {
465 const CENTER: Point = Point::new(100., 100.);
466 const RADIUS: f64 = 10.0;
467 let circle = Circle::new(CENTER, RADIUS);
468 const STRETCH: f64 = 2.717;
469 let ellipse = Affine::scale_non_uniform(STRETCH, 1.0) * circle;
470 let expected_area = PI * RADIUS.powi(2) * STRETCH;
471 assert!((ellipse.area() - expected_area).abs() < 1e-9);
472 const R_DELTA: f64 = 2.0;
473 const EXPAND: Diagonal2 = Diagonal2::new(R_DELTA * STRETCH, R_DELTA);
474 let expanded = expand_path(ellipse, EXPAND, Join::Bevel, 4.0, 1e-3);
475 let expected_area_expanded = PI * (RADIUS + R_DELTA).powi(2) * STRETCH;
476 assert!((expanded.area() - expected_area_expanded).abs() < 4e-2);
477 }
478
479 #[test]
480 fn expand_rect() {
481 const CENTER: Point = Point::new(100., 100.);
482 const SIZE: Size = Size::new(30., 20.);
483 let rect = Rect::from_center_size(CENTER, SIZE);
484 assert!((rect.area() - SIZE.area()).abs() < 1e-9);
485 const EXPAND: Diagonal2 = Diagonal2::new(10.0, 10.0);
486 let mitered = expand_path(rect, EXPAND, Join::Miter, 4.0, 1e-3);
487 let expected_area_mitered =
488 (SIZE.width + 2.0 * EXPAND.xx) * (SIZE.height + 2.0 * EXPAND.yy);
489 assert!((mitered.area() - expected_area_mitered).abs() < 1e-9);
490
491 let beveled = expand_path(rect, EXPAND, Join::Bevel, 4.0, 1e-3);
492 let expected_area_beveled = expected_area_mitered - 2.0 * EXPAND.xx * EXPAND.yy;
493 assert!((beveled.area() - expected_area_beveled).abs() < 1e-9);
494
495 let rounded = expand_path(rect, EXPAND, Join::Round, 4.0, 0.1);
496 let expected_area_rounded = expected_area_mitered - (4.0 - PI) * EXPAND.xx * EXPAND.yy;
497 assert!((rounded.area() - expected_area_rounded).abs() < 1e-1);
498 }
499
500 #[test]
501 fn expand_rounding_tolerance() {
502 const CENTER: Point = Point::new(100., 100.);
503 const SIZE: Size = Size::new(30., 20.);
504 let rect = Rect::from_center_size(CENTER, SIZE);
505 const EXPAND: Diagonal2 = Diagonal2::new(10.0, 10.0);
506 let expected_area_mitered =
507 (SIZE.width + 2.0 * EXPAND.xx) * (SIZE.height + 2.0 * EXPAND.yy);
508 let expected_area_rounded = expected_area_mitered - (4.0 - PI) * EXPAND.xx * EXPAND.yy;
509 let rounded = expand_path(rect, EXPAND, Join::Round, 4.0, 1e-3);
510 assert!((rounded.area() - expected_area_rounded).abs() < 2e-3);
511 let rounded_fine = expand_path(rect, EXPAND, Join::Round, 4.0, 1e-6);
513 assert!((rounded_fine.area() - expected_area_rounded).abs() < 3e-5);
514 }
515
516 #[test]
517 fn expand_glyph_shape() {
518 let path = BezPath::from_svg(
526 "M10.359375,-0.359375 Q6.484375,-0.359375 4.0625,2.1875 Q1.640625,4.734375 1.640625,8.984375 \
527 L1.640625,9.578125 Q1.640625,12.40625 2.71875,14.625 Q3.796875,16.859375 5.734375,18.109375 \
528 Q7.6875,19.375 9.953125,19.375 Q13.65625,19.375 15.703125,16.921875 Q17.765625,14.484375 17.765625,9.9375 \
529 L17.765625,8.578125 L4.890625,8.578125 Q4.953125,5.765625 6.53125,4.03125 Q8.109375,2.296875 10.53125,2.296875 \
530 Q12.25,2.296875 13.4375,3 Q14.640625,3.703125 15.546875,4.875 L17.53125,3.328125 Q15.140625,-0.359375 10.359375,-0.359375 Z \
531 M9.953125,16.703125 Q7.984375,16.703125 6.640625,15.265625 Q5.3125,13.828125 5,11.25 L14.515625,11.25 L14.515625,11.5 \
532 Q14.375,13.96875 13.171875,15.328125 Q11.984375,16.703125 9.953125,16.703125 Z",
533 )
534 .unwrap();
535 const EXPAND: Diagonal2 = Diagonal2::new(1.5, 1.0);
536 let expanded = expand_path(&path, EXPAND, Join::Round, 4.0, 0.1);
537 const EXPECTED_AREA: f64 = -291.3217958410297;
540 const EXPECTED_PERIMETER: f64 = 120.89147879578239;
541 const EXPECTED_MOMENT_X: f64 = -2788.568539588466;
542 const EXPECTED_MOMENT_Y: f64 = -2801.3818805722685;
543 const EXPECTED_MOMENT_XX: f64 = -34280.940819978576;
544 const EXPECTED_MOMENT_XY: f64 = -26971.281063345254;
545 const EXPECTED_MOMENT_YY: f64 = -36722.298990246876;
546 assert!((expanded.area() - EXPECTED_AREA).abs() < 1e-3);
547 assert!((expanded.perimeter(1e-9) - EXPECTED_PERIMETER).abs() < 1e-3);
548 let moments = expanded.moments();
549 assert!((moments.moment_x - EXPECTED_MOMENT_X).abs() < 1e-3);
550 assert!((moments.moment_y - EXPECTED_MOMENT_Y).abs() < 1e-3);
551 assert!((moments.moment_xx - EXPECTED_MOMENT_XX).abs() < 1e-3);
552 assert!((moments.moment_xy - EXPECTED_MOMENT_XY).abs() < 1e-3);
553 assert!((moments.moment_yy - EXPECTED_MOMENT_YY).abs() < 1e-3);
554 }
555
556 #[test]
557 fn assert_expand_subpath_closed() {
558 let mut path = BezPath::new();
559 path.move_to((0.0, 0.0));
560 path.line_to((100.0, 0.0));
561 path.line_to((100.0, 100.0));
562 path.line_to((0.0, 100.0));
563
564 let expanded = expand_path(path, Diagonal2::new(10.0, 10.0), Join::Miter, 4.0, 1e-3);
565 assert_eq!(expanded.elements().last(), Some(&PathEl::ClosePath));
566 }
567
568 #[test]
569 fn expand_degenerate_input() {
570 let path = BezPath::from_svg("M0,0 C0,0 0,0 0,0 Z").unwrap();
571 let expanded = expand_path(path, Diagonal2::new(10.0, 10.0), Join::Miter, 4.0, 1e-3);
572 assert!(expanded.is_empty());
573 }
574
575 #[test]
576 fn expand_degenerate_ghost() {
577 let mut path = BezPath::new();
578 path.extend(Rect::new(0.0, 0.0, 10.0, 10.0).path_elements(1e-3));
579 let expected = expand_path(&path, Diagonal2::new(1.0, 1.0), Join::Miter, 4.0, 1e-3);
580
581 path.move_to((20.0, 20.0));
582 path.close_path();
583 let actual = expand_path(&path, Diagonal2::new(1.0, 1.0), Join::Miter, 4.0, 1e-3);
584
585 assert_eq!(actual, expected);
586 }
587
588 #[test]
589 fn expand_open_subpath_uses_implicit_close_for_area_sign() {
590 let mut open = BezPath::new();
591 open.move_to((100.0, 100.0));
592 open.line_to((110.0, 100.0));
593 open.line_to((100.0, 110.0));
594
595 let mut closed = open.clone();
596 closed.close_path();
597
598 let expand = Diagonal2::new(1.0, 1.0);
599 let open_expanded = expand_path(open, expand, Join::Miter, 4.0, 1e-3);
600 let closed_expanded = expand_path(closed, expand, Join::Miter, 4.0, 1e-3);
601
602 assert_eq!(open_expanded, closed_expanded);
603 }
604}