|
| 1 | +use super::intersection::bezpath_intersections; |
1 | 2 | use super::poisson_disk::poisson_disk_sample;
|
| 3 | +use super::util::segment_tangent; |
2 | 4 | use crate::vector::algorithms::offset_subpath::MAX_ABSOLUTE_DIFFERENCE;
|
3 |
| -use crate::vector::misc::{PointSpacingType, dvec2_to_point}; |
4 |
| -use glam::DVec2; |
| 5 | +use crate::vector::misc::{PointSpacingType, dvec2_to_point, point_to_dvec2}; |
| 6 | +use glam::{DMat2, DVec2}; |
5 | 7 | use kurbo::{BezPath, CubicBez, DEFAULT_ACCURACY, Line, ParamCurve, ParamCurveDeriv, PathEl, PathSeg, Point, QuadBez, Rect, Shape};
|
| 8 | +use std::f64::consts::{FRAC_PI_2, PI}; |
6 | 9 |
|
7 |
| -/// Splits the [`BezPath`] at `t` value which lie in the range of [0, 1]. |
| 10 | +/// Splits the [`BezPath`] at segment index at `t` value which lie in the range of [0, 1]. |
8 | 11 | /// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1.
|
9 |
| -pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> { |
| 12 | +pub fn split_bezpath_at_segment(bezpath: &BezPath, segment_index: usize, t: f64) -> Option<(BezPath, BezPath)> { |
10 | 13 | if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 {
|
11 | 14 | return None;
|
12 | 15 | }
|
13 | 16 |
|
14 | 17 | // Get the segment which lies at the split.
|
15 |
| - let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None); |
16 | 18 | let segment = bezpath.get_seg(segment_index + 1).unwrap();
|
17 | 19 |
|
18 | 20 | // Divide the segment.
|
@@ -53,7 +55,19 @@ pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezP
|
53 | 55 | Some((first_bezpath, second_bezpath))
|
54 | 56 | }
|
55 | 57 |
|
56 |
| -pub fn position_on_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point { |
| 58 | +/// Splits the [`BezPath`] at a `t` value which lies in the range of [0, 1]. |
| 59 | +/// Returns [`None`] if the given [`BezPath`] has no segments or `t` is within f64::EPSILON of 0 or 1. |
| 60 | +pub fn split_bezpath(bezpath: &BezPath, t: f64, euclidian: bool) -> Option<(BezPath, BezPath)> { |
| 61 | + if t <= f64::EPSILON || (1. - t) <= f64::EPSILON || bezpath.segments().count() == 0 { |
| 62 | + return None; |
| 63 | + } |
| 64 | + |
| 65 | + // Get the segment which lies at the split. |
| 66 | + let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, None); |
| 67 | + split_bezpath_at_segment(bezpath, segment_index, t) |
| 68 | +} |
| 69 | + |
| 70 | +pub fn evaluate_bezpath(bezpath: &BezPath, t: f64, euclidian: bool, segments_length: Option<&[f64]>) -> Point { |
57 | 71 | let (segment_index, t) = t_value_to_parametric(bezpath, t, euclidian, segments_length);
|
58 | 72 | bezpath.get_seg(segment_index + 1).unwrap().eval(t)
|
59 | 73 | }
|
@@ -328,3 +342,117 @@ pub fn is_linear(segment: &PathSeg) -> bool {
|
328 | 342 | PathSeg::Cubic(CubicBez { p0, p1, p2, p3 }) => is_colinear(p0, p1, p3) && is_colinear(p0, p2, p3),
|
329 | 343 | }
|
330 | 344 | }
|
| 345 | + |
| 346 | +// TODO: If a segment curls back on itself tightly enough it could intersect again at the portion that should be trimmed. This could cause the Subpaths to be clipped |
| 347 | +// TODO: at the incorrect location. This can be avoided by first trimming the two Subpaths at any extrema, effectively ignoring loopbacks. |
| 348 | +/// Helper function to clip overlap of two intersecting open BezPaths. Returns an Option because intersections may not exist for certain arrangements and distances. |
| 349 | +/// Assumes that the BezPaths represents simple Bezier segments, and clips the BezPaths at the last intersection of the first BezPath, and first intersection of the last BezPath. |
| 350 | +pub fn clip_simple_bezpaths(bezpath1: &BezPath, bezpath2: &BezPath) -> Option<(BezPath, BezPath)> { |
| 351 | + // Split the first subpath at its last intersection |
| 352 | + let subpath_1_intersections = bezpath_intersections(bezpath1, bezpath2, None, None); |
| 353 | + if subpath_1_intersections.is_empty() { |
| 354 | + return None; |
| 355 | + } |
| 356 | + let (segment_index, t) = *subpath_1_intersections.last()?; |
| 357 | + let (clipped_subpath1, _) = split_bezpath_at_segment(bezpath1, segment_index, t)?; |
| 358 | + |
| 359 | + // Split the second subpath at its first intersection |
| 360 | + let subpath_2_intersections = bezpath_intersections(bezpath2, bezpath1, None, None); |
| 361 | + if subpath_2_intersections.is_empty() { |
| 362 | + return None; |
| 363 | + } |
| 364 | + let (segment_index, t) = subpath_2_intersections[0]; |
| 365 | + let (_, clipped_subpath2) = split_bezpath_at_segment(bezpath2, segment_index, t)?; |
| 366 | + |
| 367 | + Some((clipped_subpath1, clipped_subpath2)) |
| 368 | +} |
| 369 | + |
| 370 | +/// Returns the [`PathEl`] that is needed for a miter join if it is possible. |
| 371 | +/// |
| 372 | +/// `miter_limit` defines a limit for the ratio between the miter length and the stroke width. |
| 373 | +/// Alternatively, this can be interpreted as limiting the angle that the miter can form. |
| 374 | +/// When the limit is exceeded, no [`PathEl`] will be returned. |
| 375 | +/// This value should be greater than 0. If not, the default of 4 will be used. |
| 376 | +pub fn miter_line_join(bezpath1: &BezPath, bezpath2: &BezPath, miter_limit: Option<f64>) -> Option<[PathEl; 2]> { |
| 377 | + let miter_limit = match miter_limit { |
| 378 | + Some(miter_limit) if miter_limit > f64::EPSILON => miter_limit, |
| 379 | + _ => 4., |
| 380 | + }; |
| 381 | + // TODO: Besides returning None using the `?` operator, is there a more appropriate way to handle a `None` result from `get_segment`? |
| 382 | + let in_segment = bezpath1.segments().last()?; |
| 383 | + let out_segment = bezpath2.segments().next()?; |
| 384 | + |
| 385 | + let in_tangent = segment_tangent(in_segment, 1.); |
| 386 | + let out_tangent = segment_tangent(out_segment, 0.); |
| 387 | + |
| 388 | + if in_tangent == DVec2::ZERO || out_tangent == DVec2::ZERO { |
| 389 | + // Avoid panic from normalizing zero vectors |
| 390 | + // TODO: Besides returning None, is there a more appropriate way to handle this? |
| 391 | + return None; |
| 392 | + } |
| 393 | + |
| 394 | + let angle = (in_tangent * -1.).angle_to(out_tangent).abs(); |
| 395 | + |
| 396 | + if angle.to_degrees() < miter_limit { |
| 397 | + return None; |
| 398 | + } |
| 399 | + |
| 400 | + let p1 = in_segment.end(); |
| 401 | + let p2 = point_to_dvec2(p1) + in_tangent.normalize(); |
| 402 | + let line1 = Line::new(p1, dvec2_to_point(p2)); |
| 403 | + |
| 404 | + let p1 = out_segment.start(); |
| 405 | + let p2 = point_to_dvec2(p1) + out_tangent.normalize(); |
| 406 | + let line2 = Line::new(p1, dvec2_to_point(p2)); |
| 407 | + |
| 408 | + // If we don't find the intersection point to draw the miter join, we instead default to a bevel join. |
| 409 | + // Otherwise, we return the element to create the join. |
| 410 | + let intersection = line1.crossing_point(line2)?; |
| 411 | + |
| 412 | + Some([PathEl::LineTo(intersection), PathEl::LineTo(out_segment.start())]) |
| 413 | +} |
| 414 | + |
| 415 | +/// Computes the [`PathEl`] to form a circular join from `left` to `right`, along a circle around `center`. |
| 416 | +/// By default, the angle is assumed to be 180 degrees. |
| 417 | +pub fn compute_circular_subpath_details(left: DVec2, arc_point: DVec2, right: DVec2, center: DVec2, angle: Option<f64>) -> [PathEl; 2] { |
| 418 | + let center_to_arc_point = arc_point - center; |
| 419 | + |
| 420 | + // Based on https://pomax.github.io/bezierinfo/#circles_cubic |
| 421 | + let handle_offset_factor = if let Some(angle) = angle { 4. / 3. * (angle / 4.).tan() } else { 0.551784777779014 }; |
| 422 | + |
| 423 | + let p1 = dvec2_to_point(left - (left - center).perp() * handle_offset_factor); |
| 424 | + let p2 = dvec2_to_point(arc_point + center_to_arc_point.perp() * handle_offset_factor); |
| 425 | + let p3 = dvec2_to_point(arc_point); |
| 426 | + |
| 427 | + let first_half = PathEl::CurveTo(p1, p2, p3); |
| 428 | + |
| 429 | + let p1 = dvec2_to_point(arc_point - center_to_arc_point.perp() * handle_offset_factor); |
| 430 | + let p2 = dvec2_to_point(right + (right - center).perp() * handle_offset_factor); |
| 431 | + let p3 = dvec2_to_point(right); |
| 432 | + |
| 433 | + let second_half = PathEl::CurveTo(p1, p2, p3); |
| 434 | + |
| 435 | + [first_half, second_half] |
| 436 | +} |
| 437 | + |
| 438 | +/// Returns two [`PathEl`] to create a round join with the provided center. |
| 439 | +pub fn round_line_join(bezpath1: &BezPath, bezpath2: &BezPath, center: DVec2) -> [PathEl; 2] { |
| 440 | + let left = point_to_dvec2(bezpath1.segments().last().unwrap().end()); |
| 441 | + let right = point_to_dvec2(bezpath2.segments().next().unwrap().start()); |
| 442 | + |
| 443 | + let center_to_right = right - center; |
| 444 | + let center_to_left = left - center; |
| 445 | + |
| 446 | + let in_segment = bezpath1.segments().last(); |
| 447 | + let in_tangent = in_segment.map(|in_segment| segment_tangent(in_segment, 1.)); |
| 448 | + |
| 449 | + let mut angle = center_to_right.angle_to(center_to_left) / 2.; |
| 450 | + let mut arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); |
| 451 | + |
| 452 | + if in_tangent.map(|in_tangent| (arc_point - left).angle_to(in_tangent).abs()).unwrap_or_default() > FRAC_PI_2 { |
| 453 | + angle = angle - PI * (if angle < 0. { -1. } else { 1. }); |
| 454 | + arc_point = center + DMat2::from_angle(angle).mul_vec2(center_to_right); |
| 455 | + } |
| 456 | + |
| 457 | + compute_circular_subpath_details(left, arc_point, right, center, Some(angle)) |
| 458 | +} |
0 commit comments