|
| 1 | +use crate::vector::PointId; |
| 2 | +use bezier_rs::{Bezier, BezierHandles, Join, Subpath, TValue}; |
| 3 | + |
| 4 | +/// Value to control smoothness and mathematical accuracy to offset a cubic Bezier. |
| 5 | +const CUBIC_REGULARIZATION_ACCURACY: f64 = 0.5; |
| 6 | +/// Accuracy of fitting offset curve to Bezier paths. |
| 7 | +const CUBIC_TO_BEZPATH_ACCURACY: f64 = 1e-3; |
| 8 | +/// Constant used to determine if `f64`s are equivalent. |
| 9 | +pub const MAX_ABSOLUTE_DIFFERENCE: f64 = 1e-3; |
| 10 | + |
| 11 | +fn segment_to_bezier(seg: kurbo::PathSeg) -> bezier_rs::Bezier { |
| 12 | + match seg { |
| 13 | + kurbo::PathSeg::Line(line) => Bezier::from_linear_coordinates(line.p0.x, line.p0.y, line.p1.x, line.p1.y), |
| 14 | + kurbo::PathSeg::Quad(quad_bez) => Bezier::from_quadratic_coordinates(quad_bez.p0.x, quad_bez.p0.y, quad_bez.p1.x, quad_bez.p1.y, quad_bez.p1.x, quad_bez.p1.y), |
| 15 | + kurbo::PathSeg::Cubic(cubic_bez) => Bezier::from_cubic_coordinates( |
| 16 | + cubic_bez.p0.x, |
| 17 | + cubic_bez.p0.y, |
| 18 | + cubic_bez.p1.x, |
| 19 | + cubic_bez.p1.y, |
| 20 | + cubic_bez.p2.x, |
| 21 | + cubic_bez.p2.y, |
| 22 | + cubic_bez.p3.x, |
| 23 | + cubic_bez.p3.y, |
| 24 | + ), |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +// TODO: Replace the implementation to use only Kurbo API. |
| 29 | +/// Reduces the segments of the subpath into simple subcurves, then offset each subcurve a set `distance` away. |
| 30 | +/// The intersections of segments of the subpath are joined using the method specified by the `join` argument. |
| 31 | +pub fn offset_subpath(subpath: &Subpath<PointId>, distance: f64, join: Join) -> Subpath<PointId> { |
| 32 | + // An offset at a distance 0 from the curve is simply the same curve. |
| 33 | + // An offset of a single point is not defined. |
| 34 | + if distance == 0. || subpath.len() <= 1 || subpath.len_segments() < 1 { |
| 35 | + return subpath.clone(); |
| 36 | + } |
| 37 | + |
| 38 | + let mut subpaths = subpath |
| 39 | + .iter() |
| 40 | + .filter(|bezier| !bezier.is_point()) |
| 41 | + .map(|bezier| bezier.to_cubic()) |
| 42 | + .map(|cubic| { |
| 43 | + let Bezier { start, end, handles } = cubic; |
| 44 | + let BezierHandles::Cubic { handle_start, handle_end } = handles else { unreachable!()}; |
| 45 | + |
| 46 | + let cubic_bez = kurbo::CubicBez::new((start.x, start.y), (handle_start.x, handle_start.y), (handle_end.x, handle_end.y), (end.x, end.y)); |
| 47 | + let cubic_offset = kurbo::offset::CubicOffset::new_regularized(cubic_bez, distance, CUBIC_REGULARIZATION_ACCURACY); |
| 48 | + let offset_bezpath = kurbo::fit_to_bezpath(&cubic_offset, CUBIC_TO_BEZPATH_ACCURACY); |
| 49 | + |
| 50 | + let beziers = offset_bezpath.segments().fold(Vec::new(), |mut acc, seg| { |
| 51 | + acc.push(segment_to_bezier(seg)); |
| 52 | + acc |
| 53 | + }); |
| 54 | + |
| 55 | + Subpath::from_beziers(&beziers, false) |
| 56 | + }) |
| 57 | + .filter(|subpath| subpath.len() >= 2) // In some cases the reduced and scaled bézier is marked by is_point (so the subpath is empty). |
| 58 | + .collect::<Vec<Subpath<PointId>>>(); |
| 59 | + |
| 60 | + let mut drop_common_point = vec![true; subpath.len()]; |
| 61 | + |
| 62 | + // Clip or join consecutive Subpaths |
| 63 | + for i in 0..subpaths.len() - 1 { |
| 64 | + let j = i + 1; |
| 65 | + let subpath1 = &subpaths[i]; |
| 66 | + let subpath2 = &subpaths[j]; |
| 67 | + |
| 68 | + let last_segment = subpath1.get_segment(subpath1.len_segments() - 1).unwrap(); |
| 69 | + let first_segment = subpath2.get_segment(0).unwrap(); |
| 70 | + |
| 71 | + // If the anchors are approximately equal, there is no need to clip / join the segments |
| 72 | + if last_segment.end().abs_diff_eq(first_segment.start(), MAX_ABSOLUTE_DIFFERENCE) { |
| 73 | + continue; |
| 74 | + } |
| 75 | + |
| 76 | + // Calculate the angle formed between two consecutive Subpaths |
| 77 | + let out_tangent = subpath.get_segment(i).unwrap().tangent(TValue::Parametric(1.)); |
| 78 | + let in_tangent = subpath.get_segment(j).unwrap().tangent(TValue::Parametric(0.)); |
| 79 | + let angle = out_tangent.angle_to(in_tangent); |
| 80 | + |
| 81 | + // The angle is concave. The Subpath overlap and must be clipped |
| 82 | + let mut apply_join = true; |
| 83 | + if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) { |
| 84 | + // If the distance is large enough, there may still be no intersections. Also, if the angle is close enough to zero, |
| 85 | + // subpath intersections may find no intersections. In this case, the points are likely close enough that we can approximate |
| 86 | + // the points as being on top of one another. |
| 87 | + if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(subpath1, subpath2) { |
| 88 | + subpaths[i] = clipped_subpath1; |
| 89 | + subpaths[j] = clipped_subpath2; |
| 90 | + apply_join = false; |
| 91 | + } |
| 92 | + } |
| 93 | + // The angle is convex. The Subpath must be joined using the specified join type |
| 94 | + if apply_join { |
| 95 | + drop_common_point[j] = false; |
| 96 | + match join { |
| 97 | + Join::Bevel => {} |
| 98 | + Join::Miter(miter_limit) => { |
| 99 | + let miter_manipulator_group = subpaths[i].miter_line_join(&subpaths[j], miter_limit); |
| 100 | + if let Some(miter_manipulator_group) = miter_manipulator_group { |
| 101 | + subpaths[i].manipulator_groups_mut().push(miter_manipulator_group); |
| 102 | + } |
| 103 | + } |
| 104 | + Join::Round => { |
| 105 | + let (out_handle, round_point, in_handle) = subpaths[i].round_line_join(&subpaths[j], subpath.manipulator_groups()[j].anchor); |
| 106 | + let last_index = subpaths[i].manipulator_groups().len() - 1; |
| 107 | + subpaths[i].manipulator_groups_mut()[last_index].out_handle = Some(out_handle); |
| 108 | + subpaths[i].manipulator_groups_mut().push(round_point); |
| 109 | + subpaths[j].manipulator_groups_mut()[0].in_handle = Some(in_handle); |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + } |
| 114 | + |
| 115 | + // Clip any overlap in the last segment |
| 116 | + if subpath.closed { |
| 117 | + let out_tangent = subpath.get_segment(subpath.len_segments() - 1).unwrap().tangent(TValue::Parametric(1.)); |
| 118 | + let in_tangent = subpath.get_segment(0).unwrap().tangent(TValue::Parametric(0.)); |
| 119 | + let angle = out_tangent.angle_to(in_tangent); |
| 120 | + |
| 121 | + let mut apply_join = true; |
| 122 | + if (angle > 0. && distance > 0.) || (angle < 0. && distance < 0.) { |
| 123 | + if let Some((clipped_subpath1, clipped_subpath2)) = Subpath::clip_simple_subpaths(&subpaths[subpaths.len() - 1], &subpaths[0]) { |
| 124 | + // Merge the clipped subpaths |
| 125 | + let last_index = subpaths.len() - 1; |
| 126 | + subpaths[last_index] = clipped_subpath1; |
| 127 | + subpaths[0] = clipped_subpath2; |
| 128 | + apply_join = false; |
| 129 | + } |
| 130 | + } |
| 131 | + if apply_join { |
| 132 | + drop_common_point[0] = false; |
| 133 | + match join { |
| 134 | + Join::Bevel => {} |
| 135 | + Join::Miter(miter_limit) => { |
| 136 | + let last_subpath_index = subpaths.len() - 1; |
| 137 | + let miter_manipulator_group = subpaths[last_subpath_index].miter_line_join(&subpaths[0], miter_limit); |
| 138 | + if let Some(miter_manipulator_group) = miter_manipulator_group { |
| 139 | + subpaths[last_subpath_index].manipulator_groups_mut().push(miter_manipulator_group); |
| 140 | + } |
| 141 | + } |
| 142 | + Join::Round => { |
| 143 | + let last_subpath_index = subpaths.len() - 1; |
| 144 | + let (out_handle, round_point, in_handle) = subpaths[last_subpath_index].round_line_join(&subpaths[0], subpath.manipulator_groups()[0].anchor); |
| 145 | + let last_index = subpaths[last_subpath_index].manipulator_groups().len() - 1; |
| 146 | + subpaths[last_subpath_index].manipulator_groups_mut()[last_index].out_handle = Some(out_handle); |
| 147 | + subpaths[last_subpath_index].manipulator_groups_mut().push(round_point); |
| 148 | + subpaths[0].manipulator_groups_mut()[0].in_handle = Some(in_handle); |
| 149 | + } |
| 150 | + } |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + // Merge the subpaths. Drop points which overlap with one another. |
| 155 | + let mut manipulator_groups = subpaths[0].manipulator_groups().to_vec(); |
| 156 | + for i in 1..subpaths.len() { |
| 157 | + if drop_common_point[i] { |
| 158 | + let last_group = manipulator_groups.pop().unwrap(); |
| 159 | + let mut manipulators_copy = subpaths[i].manipulator_groups().to_vec(); |
| 160 | + manipulators_copy[0].in_handle = last_group.in_handle; |
| 161 | + |
| 162 | + manipulator_groups.append(&mut manipulators_copy); |
| 163 | + } else { |
| 164 | + manipulator_groups.append(&mut subpaths[i].manipulator_groups().to_vec()); |
| 165 | + } |
| 166 | + } |
| 167 | + if subpath.closed && drop_common_point[0] { |
| 168 | + let last_group = manipulator_groups.pop().unwrap(); |
| 169 | + manipulator_groups[0].in_handle = last_group.in_handle; |
| 170 | + } |
| 171 | + |
| 172 | + Subpath::new(manipulator_groups, subpath.closed) |
| 173 | +} |
0 commit comments