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 }