File indexing completed on 2025-02-02 04:11:07

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "offset_path.hpp"
0008 #include "math/geom.hpp"
0009 #include "math/vector.hpp"
0010 
0011 GLAXNIMATE_OBJECT_IMPL(glaxnimate::model::OffsetPath)
0012 
0013 using namespace glaxnimate;
0014 using namespace glaxnimate::math::bezier;
0015 
0016 static bool point_fuzzy_compare(const QPointF& a, const QPointF& b)
0017 {
0018     return qFuzzyCompare(a.x(), b.x()) && qFuzzyCompare(a.y(), b.y());
0019 }
0020 
0021 /*
0022     Simple offset of a linear segment
0023 */
0024 static std::pair<QPointF, QPointF> linear_offset(const QPointF& p1, const QPointF& p2, float amount)
0025 {
0026     auto angle = math::atan2(p2.x() - p1.x(), p2.y() - p1.y());
0027     auto offset = math::from_polar<QPointF>(amount, -angle);
0028     return {
0029         p1 + offset,
0030         p2 + offset
0031     };
0032 }
0033 
0034 template<int size>
0035 static std::array<QPointF, size> offset_polygon(std::array<QPointF, size> points, float amount)
0036 {
0037     std::array<std::pair<QPointF, QPointF>, size - 1> off_lines;
0038 
0039     for ( int i = 1; i < size; i++ )
0040     {
0041         off_lines[i-1] = linear_offset(points[i-1], points[i], amount);
0042     }
0043 
0044     std::array<QPointF, size> off_points;
0045     off_points[0] = off_lines[0].first;
0046     off_points[size - 1] = off_lines.back().second;
0047     for ( int i = 1; i < size - 1; i++ )
0048     {
0049         off_points[i] = math::line_intersection(off_lines[i-1].first, off_lines[i-1].second, off_lines[i].first, off_lines[i].second).value_or(off_lines[i].first);
0050     }
0051     return off_points;
0052 }
0053 
0054 /*
0055     Offset a bezier segment
0056     only works well if the segment is flat enough
0057 */
0058 static math::bezier::CubicBezierSolver<QPointF> offset_segment(const math::bezier::CubicBezierSolver<QPointF>& segment, float amount)
0059 {
0060     bool same01 = point_fuzzy_compare(segment.points()[0], segment.points()[1]);
0061     bool same23 = point_fuzzy_compare(segment.points()[2], segment.points()[3]);
0062     if ( same01 && same23 )
0063     {
0064         auto off = linear_offset(segment.points()[0], segment.points().back(), amount);
0065         return {off.first, math::lerp(off.first, off.second, 1./3.), math::lerp(off.first, off.second, 2./3.), off.second};
0066     }
0067     else if ( same01 )
0068     {
0069         auto poly = offset_polygon<3>({segment.points()[0], segment.points()[2], segment.points()[3]}, amount);
0070         return {poly[0], poly[0], poly[1], poly[2]};
0071     }
0072     else if ( same23 )
0073     {
0074         auto poly = offset_polygon<3>({segment.points()[0], segment.points()[1], segment.points()[3]}, amount);
0075         return {poly[0], poly[1], poly[2], poly[2]};
0076     }
0077 
0078     return offset_polygon<4>(segment.points(), amount);
0079 
0080 }
0081 
0082 /*
0083     Join two segments
0084 */
0085 static QPointF join_lines(
0086     Bezier& output_bezier,
0087     const CubicBezierSolver<QPointF>& seg1,
0088     const CubicBezierSolver<QPointF>& seg2,
0089     glaxnimate::model::Stroke::Join line_join,
0090     float miter_limit
0091 )
0092 {
0093     QPointF p0 = seg1.points()[3];
0094     QPointF p1 = seg2.points()[0];
0095 
0096    if ( line_join == glaxnimate::model::Stroke::BevelJoin )
0097         return p0;
0098 
0099     // Connected, they don't need a joint
0100     if ( point_fuzzy_compare(p0, p1) )
0101         return p0;
0102 
0103     auto& last_point = output_bezier.points().back();
0104 
0105     if ( line_join == glaxnimate::model::Stroke::RoundJoin )
0106     {
0107         auto angle_out = seg1.tangent_angle(1);
0108         auto angle_in = seg2.tangent_angle(0) + math::pi;
0109         auto offset = math::from_polar<QPointF>(100, angle_out + math::pi / 2);
0110         auto center = math::line_intersection(p0, p0 + offset, p1, p1 + offset);
0111         auto radius = center ? math::distance(*center, p0) : math::distance(p0, p1) / 2;
0112         last_point.tan_out = last_point.pos +
0113             math::from_polar<QPointF>(2 * radius * math::ellipse_bezier, angle_out);
0114 
0115         output_bezier.add_point(p1, math::from_polar<QPointF>(2 * radius * math::ellipse_bezier, angle_in));
0116 
0117         return p1;
0118     }
0119 
0120     // Miter
0121     auto t0 = point_fuzzy_compare(p0, seg1.points()[2]) ? seg1.points()[0] : seg1.points()[2];
0122     auto t1 = point_fuzzy_compare(p1, seg2.points()[1]) ? seg2.points()[3] : seg2.points()[1];
0123     auto intersection = math::line_intersection(t0, p0, p1, t1);
0124 
0125     if ( intersection && math::distance(*intersection, p0) < miter_limit )
0126     {
0127         output_bezier.add_point(*intersection);
0128         return *intersection;
0129     }
0130 
0131     return p0;
0132 }
0133 
0134 
0135 static std::optional<std::pair<float, float>> get_intersection(
0136     const CubicBezierSolver<QPointF>&a,
0137     const CubicBezierSolver<QPointF>& b)
0138 {
0139     auto intersect = a.intersections(b, 2, 3, 7);
0140 
0141     std::size_t i = 0;
0142     if ( !intersect.empty() && qFuzzyCompare(intersect[0].first, 1) )
0143         i++;
0144 
0145     if ( intersect.size() > i )
0146         return intersect[i];
0147 
0148     return {};
0149 }
0150 
0151 static std::pair<std::vector<CubicBezierSolver<QPointF>>, std::vector<CubicBezierSolver<QPointF>>>
0152 prune_segment_intersection(
0153     const std::vector<CubicBezierSolver<QPointF>>& a,
0154     const std::vector<CubicBezierSolver<QPointF>>& b
0155 )
0156 {
0157     auto out_a = a;
0158     auto out_b = b;
0159 
0160     auto intersect = get_intersection(a.back(), b[0]);
0161 
0162     if ( intersect )
0163     {
0164         out_a.back() = a.back().split(intersect->first).first;
0165         out_b[0] = b[0].split(intersect->second).second;
0166     }
0167 
0168     if ( a.size() > 1 && b.size() > 1 )
0169     {
0170         intersect = get_intersection(a[0], b.back());
0171 
0172         if ( intersect )
0173         {
0174             return {
0175                 {a[0].split(intersect->first).first},
0176                 {b.back().split(intersect->second).second},
0177             };
0178         }
0179     }
0180 
0181     return {out_a, out_b};
0182 }
0183 
0184 void prune_intersections(std::vector<std::vector<CubicBezierSolver<QPointF>>>& segments)
0185 {
0186     for ( std::size_t i = 1; i < segments.size() ; i++ )
0187     {
0188         std::tie(segments[i-1], segments[i]) = prune_segment_intersection(segments[i - 1], segments[i]);
0189     }
0190 
0191     if ( segments.size() > 1 )
0192         std::tie(segments.back(), segments[0]) = prune_segment_intersection(segments.back(), segments[0]);
0193 
0194 }
0195 
0196 static std::vector<math::bezier::CubicBezierSolver<QPointF>> split_inflections(
0197     const math::bezier::CubicBezierSolver<QPointF>& segment
0198 )
0199 {
0200     /*
0201         We split each bezier segment into smaller pieces based
0202         on inflection points, this ensures the control point
0203         polygon is convex.
0204 
0205         (A cubic bezier can have none, one, or two inflection points)
0206     */
0207     auto flex = segment.inflection_points();
0208 
0209     if ( flex.size() == 0 )
0210     {
0211         return {segment};
0212     }
0213     else if ( flex.size() == 1 || flex[1] == 1 )
0214     {
0215         auto split = segment.split(flex[0]);
0216 
0217         return {
0218             split.first,
0219             split.second
0220         };
0221     }
0222     else
0223     {
0224         auto split_1 = segment.split(flex[0]);
0225         float t = (flex[1] - flex[0]) / (1 - flex[0]);
0226         auto split_2 = CubicBezierSolver(split_1.second).split(t);
0227 
0228         return {
0229             split_1.first,
0230             split_2.first,
0231             split_2.second,
0232         };
0233     }
0234 }
0235 
0236 static bool needs_more_split(const math::bezier::CubicBezierSolver<QPointF>& segment)
0237 {
0238     auto n1 = math::from_polar<QPointF>(1, segment.normal_angle(0));
0239     auto n2 = math::from_polar<QPointF>(1, segment.normal_angle(1));
0240 
0241     auto s = QPointF::dotProduct(n1, n2);
0242     return math::abs(math::acos(s)) >= math::pi / 3;
0243 }
0244 
0245 static std::vector<math::bezier::CubicBezierSolver<QPointF>> offset_segment_split(
0246     const math::bezier::CubicBezierSolver<QPointF>& segment,
0247     float amount
0248 )
0249 {
0250     std::vector<math::bezier::CubicBezierSolver<QPointF>> offset;
0251     offset.reserve(6);
0252 
0253     for ( const auto& chunk: split_inflections(segment) )
0254     {
0255         if ( needs_more_split(chunk) )
0256         {
0257             auto split = chunk.split(0.5);
0258             offset.push_back(offset_segment(split.first, amount));
0259             offset.push_back(offset_segment(split.second, amount));
0260         }
0261         else
0262         {
0263             offset.push_back(offset_segment(chunk, amount));
0264         }
0265     }
0266 
0267     return offset;
0268 }
0269 
0270 static MultiBezier offset_path(
0271     // Beziers as collected from the other shapes
0272     const MultiBezier& collected_shapes,
0273     float amount,
0274     model::Stroke::Join line_join,
0275     float miter_limit
0276 )
0277 {
0278     MultiBezier result;
0279 
0280     for ( const auto& input_bezier : collected_shapes.beziers() )
0281     {
0282         int count = input_bezier.segment_count();
0283         Bezier output_bezier;
0284 
0285         output_bezier.set_closed(input_bezier.closed());
0286 
0287         std::vector<std::vector<math::bezier::CubicBezierSolver<QPointF>>> multi_segments;
0288         for ( int i = 0; i < count; i++ )
0289             multi_segments.push_back(offset_segment_split(input_bezier.segment(i), amount));
0290 
0291         // Open paths are stroked rather than being simply offset
0292         if ( !input_bezier.closed() )
0293         {
0294             for ( int i = count - 1; i >= 0; i-- )
0295                 multi_segments.push_back(offset_segment_split(input_bezier.inverted_segment(i), amount));
0296         }
0297 
0298         prune_intersections(multi_segments);
0299 
0300         // Add bezier segments to the output and apply line joints
0301         QPointF last_point;
0302         const math::bezier::CubicBezierSolver<QPointF>* last_seg = nullptr;
0303 
0304         for ( const auto& multi_segment : multi_segments )
0305         {
0306             if ( last_seg )
0307                 last_point = join_lines(output_bezier, *last_seg, multi_segment[0], line_join, miter_limit * amount);
0308 
0309             last_seg = &multi_segment.back();
0310 
0311             for ( const auto& segment : multi_segment )
0312             {
0313                 if ( !point_fuzzy_compare(segment.points()[0], last_point) || output_bezier.empty() )
0314                     output_bezier.add_point(segment.points()[0]);
0315 
0316                 output_bezier.back().tan_out = segment.points()[1];
0317 
0318 
0319                 output_bezier.add_point(segment.points()[3]);
0320                 output_bezier.back().tan_in = segment.points()[2];
0321 
0322                 last_point = segment.points()[3];
0323             }
0324         }
0325 
0326         if ( !multi_segments.empty() )
0327             join_lines(output_bezier, *last_seg, multi_segments[0][0], line_join, miter_limit * amount);
0328 
0329         result.beziers().push_back(output_bezier);
0330     }
0331 
0332     return result;
0333 }
0334 
0335 QIcon glaxnimate::model::OffsetPath::static_tree_icon()
0336 {
0337     return QIcon::fromTheme("path-offset-dynamic");
0338 }
0339 
0340 QString glaxnimate::model::OffsetPath::static_type_name_human()
0341 {
0342     return i18n("Offset Path");
0343 }
0344 
0345 bool glaxnimate::model::OffsetPath::process_collected() const
0346 {
0347     return false;
0348 }
0349 
0350 glaxnimate::math::bezier::MultiBezier glaxnimate::model::OffsetPath::process(
0351     glaxnimate::model::FrameTime t,
0352     const math::bezier::MultiBezier& mbez
0353 ) const
0354 {
0355     if ( mbez.empty() )
0356         return {};
0357 
0358     auto amount = this->amount.get_at(t);
0359 
0360     if ( qFuzzyIsNull(amount) )
0361         return mbez;
0362 
0363     return offset_path(mbez, amount, join.get(), miter_limit.get_at(t));
0364 }