File indexing completed on 2024-12-15 04:01:12

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 "operations.hpp"
0008 #include <vector>
0009 
0010 using namespace glaxnimate;
0011 
0012 
0013 // Algorithm from https://www.particleincell.com/2012/bezier-splines/
0014 void math::bezier::auto_smooth(math::bezier::Bezier& curve, int start, int end)
0015 {
0016     if ( start < 0 || end > curve.size() || end - start < 2 )
0017         return;
0018 
0019     int n = end - start - 1;
0020 
0021     // rhs vector
0022     std::vector<qreal> a, b, c;
0023     std::vector<QPointF> r;
0024 
0025     // left most segment
0026     a.push_back(0);
0027     b.push_back(2);
0028     c.push_back(1);
0029     r.push_back(curve[start].pos + 2 * curve[start+1].pos);
0030 
0031     // internal segments
0032     for ( int i = 1; i < n - 1; i++ )
0033     {
0034         a.push_back(1);
0035         b.push_back(4);
0036         c.push_back(1);
0037         r.push_back(4 * curve[start+i].pos + 2 * curve[start+i+1].pos);
0038     }
0039 
0040     // right segment
0041     a.push_back(2);
0042     b.push_back(7);
0043     c.push_back(0);
0044     r.push_back(8 * curve[end-2].pos + curve[end-1].pos);
0045 
0046     // solves Ax=b with the Thomas algorithm (from Wikipedia)
0047     for ( int i = 1; i < n; i++ )
0048     {
0049         qreal m = a[i] / b[i-1];
0050         b[i] = b[i] - m * c[i - 1];
0051         r[i] = r[i] - m * r[i-1];
0052     }
0053 
0054     QPointF last = r[n-1]/b[n-1];
0055     curve[end-2].tan_in = last;
0056     for ( int i = n - 2; i >= 0; --i )
0057     {
0058         last = (r[i] - c[i] * last) / b[i];
0059         QPointF relative = (last - curve[start+i].pos);
0060         curve[start+i].tan_in = curve[start+i].pos - relative;
0061         curve[start+i].tan_out = curve[start+i].pos + relative;
0062         curve[start+i].type = math::bezier::Smooth;
0063     }
0064 
0065 }
0066 
0067 
0068 static qreal triangle_area(const math::bezier::Bezier& curve, int point)
0069 {
0070     QPointF prev = curve[point-1].pos;
0071     QPointF here = curve[point].pos;
0072     QPointF next = curve[point+1].pos;
0073 
0074     return qAbs(
0075         prev.x() * here.y() - here.x() * prev.y() +
0076         here.x() * next.y() - next.x() * here.y() +
0077         next.x() * prev.y() - prev.x() * next.y()
0078     );
0079 }
0080 
0081 void math::bezier::simplify(math::bezier::Bezier& curve, qreal threshold)
0082 {
0083     if ( curve.size() < 3 || threshold <= 0 )
0084         return;
0085 
0086     // Algorithm based on https://bost.ocks.org/mike/simplify/
0087     std::vector<qreal> tris;
0088 
0089     tris.reserve(curve.size());
0090 
0091     tris.push_back(threshold); // [0] not used but keeping it for my own sanity
0092     for ( int i = 1; i < curve.size() - 1; i++ )
0093         tris.push_back(triangle_area(curve, i));
0094 
0095     while ( !tris.empty() )
0096     {
0097         qreal min = threshold;
0098         int index = -1;
0099 
0100         for ( int i = 0; i < int(tris.size()); i++ )
0101         {
0102             if ( tris[i] < min )
0103             {
0104                 index = i;
0105                 min = tris[i];
0106             }
0107         }
0108         if ( index == -1 )
0109             break;
0110 
0111         tris.erase(tris.begin() + index);
0112         curve.points().erase(curve.begin() + index);
0113 
0114         if ( index < int(tris.size()) )
0115             tris[index] = triangle_area(curve, index);
0116         if ( index > 1 )
0117             tris[index-1] = triangle_area(curve, index - 1);
0118     }
0119 
0120     // Fake smoothness
0121     auto_smooth(curve, 0, curve.size());
0122 
0123 }
0124 
0125 static math::bezier::ProjectResult project_extreme(int index, qreal t, const QPointF& p)
0126 {
0127     return {index, t, math::length_squared(p), p};
0128 }
0129 
0130 static void project_impl(const math::bezier::CubicBezierSolver<QPointF>& solver, const QPointF& p, int index, math::bezier::ProjectResult& best)
0131 {
0132     static constexpr const double min_dist = 0.01;
0133 
0134     math::bezier::ProjectResult left = project_extreme(index, 0, solver.points()[0]);
0135     math::bezier::ProjectResult right = project_extreme(index, 1, solver.points()[3]);
0136     math::bezier::ProjectResult middle = {index, 0, 0, {}};
0137 
0138     while ( true )
0139     {
0140         middle.factor = (left.factor + right.factor) / 2;
0141         middle.point = solver.solve(middle.factor);
0142         middle.distance = math::length_squared(middle.point);
0143 
0144         if ( right.distance < left.distance )
0145             left = middle;
0146         else
0147             right = middle;
0148 
0149         auto len = math::length_squared(left.point - right.point);
0150         if ( len <= min_dist || !std::isfinite(len) )
0151             break;
0152     }
0153 
0154     if ( right.distance < left.distance )
0155         left = right;
0156 
0157     if ( left.distance < best.distance )
0158     {
0159         best = left;
0160         best.point += p;
0161     }
0162 }
0163 
0164 static void project_impl(const math::bezier::Bezier& curve, const QPointF& p, int index, math::bezier::ProjectResult& best)
0165 {
0166     math::bezier::CubicBezierSolver<QPointF> solver{
0167         curve[index].pos - p,
0168         curve[index].tan_out - p,
0169         curve[index + 1].tan_in - p,
0170         curve[index + 1].pos - p
0171     };
0172 
0173     project_impl(solver, p, index, best);
0174 }
0175 
0176 math::bezier::ProjectResult math::bezier::project(const math::bezier::Bezier& curve, const QPointF& p)
0177 {
0178     if ( curve.empty() )
0179         return {0, 0, 0, p};
0180 
0181     if ( curve.size() == 1 )
0182         return {0, 0, math::length_squared(curve[0].pos - p), curve[0].pos};
0183 
0184     ProjectResult best {0, 0, std::numeric_limits<qreal>::max(), curve[0].pos};
0185     for ( int i = 0; i < curve.size() - 1; i++ )
0186         project_impl(curve, p, i, best);
0187 
0188     if ( curve.closed() )
0189         project_impl(curve, p, curve.size() - 1, best);
0190 
0191     return best;
0192 }
0193 
0194 math::bezier::ProjectResult math::bezier::project(const math::bezier::BezierSegment& segment, const QPointF& p)
0195 {
0196     ProjectResult best {0, 0, std::numeric_limits<qreal>::max(), segment[0]};
0197     math::bezier::CubicBezierSolver<QPointF> solver{
0198         segment[0] - p,
0199         segment[1] - p,
0200         segment[2] - p,
0201         segment[3] - p
0202     };
0203 
0204     project_impl(solver, p, 0, best);
0205 
0206     return best;
0207 }