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 }