QGIS API Documentation 3.99.0-Master (7d2ca374f2d)
Loading...
Searching...
No Matches
qgsbezierdata.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsbezierdata.cpp - Data structure for Poly-Bézier curve digitizing
3 ---------------------
4 begin : December 2025
5 copyright : (C) 2025 by Loïc Bartoletti
6 Adapted from BezierEditing plugin work by Takayuki Mizutani
7 email : loic dot bartoletti at oslandia dot com
8 ***************************************************************************
9 * *
10 * This program is free software; you can redistribute it and/or modify *
11 * it under the terms of the GNU General Public License as published by *
12 * the Free Software Foundation; either version 2 of the License, or *
13 * (at your option) any later version. *
14 * *
15 ***************************************************************************/
16
17#include "qgsbezierdata.h"
18
19#include <cmath>
20
21#include "qgsgeometryutils.h"
22#include "qgsnurbscurve.h"
23
25
26const QgsAnchorWithHandles QgsBezierData::sInvalidAnchor;
27
28void QgsBezierData::addAnchor( const QgsPoint &point )
29{
30 mData.append( QgsAnchorWithHandles( point ) );
31}
32
33void QgsBezierData::moveAnchor( int index, const QgsPoint &point )
34{
35 if ( index < 0 || index >= mData.count() )
36 return;
37
38 QgsAnchorWithHandles &data = mData[index];
39
40 // Calculate offset
41 const double dx = point.x() - data.anchor.x();
42 const double dy = point.y() - data.anchor.y();
43 const double dz = point.is3D() ? ( point.z() - data.anchor.z() ) : 0.0;
44
45 // Move anchor
46 data.anchor = point;
47
48 // Move both handles relatively
49 data.leftHandle.setX( data.leftHandle.x() + dx );
50 data.leftHandle.setY( data.leftHandle.y() + dy );
51 if ( point.is3D() )
52 data.leftHandle.setZ( data.leftHandle.z() + dz );
53
54 data.rightHandle.setX( data.rightHandle.x() + dx );
55 data.rightHandle.setY( data.rightHandle.y() + dy );
56 if ( point.is3D() )
57 data.rightHandle.setZ( data.rightHandle.z() + dz );
58}
59
60void QgsBezierData::moveHandle( int index, const QgsPoint &point )
61{
62 const int anchorIndex = index / 2;
63 if ( anchorIndex < 0 || anchorIndex >= mData.count() )
64 return;
65
66 if ( index % 2 == 0 )
67 mData[anchorIndex].leftHandle = point;
68 else
69 mData[anchorIndex].rightHandle = point;
70}
71
72void QgsBezierData::insertAnchor( int segmentIndex, const QgsPoint &point )
73{
74 if ( segmentIndex < 0 || segmentIndex > mData.count() )
75 return;
76
77 mData.insert( segmentIndex, QgsAnchorWithHandles( point ) );
78}
79
80void QgsBezierData::deleteAnchor( int index )
81{
82 if ( index < 0 || index >= mData.count() )
83 return;
84
85 mData.removeAt( index );
86}
87
88void QgsBezierData::retractHandle( int index )
89{
90 const int anchorIndex = index / 2;
91 if ( anchorIndex < 0 || anchorIndex >= mData.count() )
92 return;
93
94 if ( index % 2 == 0 )
95 mData[anchorIndex].leftHandle = mData[anchorIndex].anchor;
96 else
97 mData[anchorIndex].rightHandle = mData[anchorIndex].anchor;
98}
99
100void QgsBezierData::extendHandle( int index, const QgsPoint &point )
101{
102 moveHandle( index, point );
103}
104
105QgsPoint QgsBezierData::anchor( int index ) const
106{
107 if ( index < 0 || index >= mData.count() )
108 return QgsPoint();
109 return mData[index].anchor;
110}
111
112QgsPoint QgsBezierData::handle( int index ) const
113{
114 const int anchorIndex = index / 2;
115 if ( anchorIndex < 0 || anchorIndex >= mData.count() )
116 return QgsPoint();
117
118 if ( index % 2 == 0 )
119 return mData[anchorIndex].leftHandle;
120 else
121 return mData[anchorIndex].rightHandle;
122}
123
124QVector<QgsPoint> QgsBezierData::anchors() const
125{
126 QVector<QgsPoint> result;
127 result.reserve( mData.count() );
128 for ( const QgsAnchorWithHandles &awh : mData )
129 result.append( awh.anchor );
130 return result;
131}
132
133QVector<QgsPoint> QgsBezierData::handles() const
134{
135 QVector<QgsPoint> result;
136 result.reserve( mData.count() * 2 );
137 for ( const QgsAnchorWithHandles &awh : mData )
138 {
139 result.append( awh.leftHandle );
140 result.append( awh.rightHandle );
141 }
142 return result;
143}
144
145const QgsAnchorWithHandles &QgsBezierData::anchorWithHandles( int index ) const
146{
147 if ( index < 0 || index >= mData.count() )
148 return sInvalidAnchor;
149 return mData[index];
150}
151
152QgsPointSequence QgsBezierData::interpolateLine() const
153{
154 QgsPointSequence result;
155
156 if ( mData.count() < 2 )
157 {
158 // Not enough anchors for a curve, just return anchors
159 for ( const QgsAnchorWithHandles &awh : mData )
160 result.append( awh.anchor );
161 return result;
162 }
163
164 // Add first anchor
165 result.append( mData.first().anchor );
166
167 // For each segment between consecutive anchors
168 for ( int i = 0; i < mData.count() - 1; ++i )
169 {
170 const QgsPoint &p0 = mData[i].anchor;
171 const QgsPoint &p1 = mData[i].rightHandle;
172 const QgsPoint &p2 = mData[i + 1].leftHandle;
173 const QgsPoint &p3 = mData[i + 1].anchor;
174
175 // Interpolate the segment
176 for ( int j = 1; j <= INTERPOLATION_POINTS; ++j )
177 {
178 const double t = static_cast<double>( j ) / INTERPOLATION_POINTS;
179 result.append( QgsGeometryUtils::interpolatePointOnCubicBezier( p0, p1, p2, p3, t ) );
180 }
181 }
182
183 return result;
184}
185
186std::unique_ptr<QgsNurbsCurve> QgsBezierData::asNurbsCurve() const
187{
188 const int anchorCount = mData.count();
189 if ( anchorCount < 2 )
190 return nullptr;
191
192 // Build control points: anchor, handle_right, handle_left, anchor, ...
193 // A piecewise cubic Bézier with n anchors has n-1 segments
194 // Total control points: 1 + 3*(n-1) = 3n-2
195 QVector<QgsPoint> ctrlPts;
196 ctrlPts.append( mData[0].anchor );
197
198 for ( int i = 0; i < anchorCount - 1; ++i )
199 {
200 ctrlPts.append( mData[i].rightHandle );
201 ctrlPts.append( mData[i + 1].leftHandle );
202 ctrlPts.append( mData[i + 1].anchor );
203 }
204
205 QVector<double> knots = QgsNurbsCurve::generateKnotsForBezierConversion( anchorCount );
206
207 // Uniform weights (non-rational B-spline)
208 QVector<double> weights( ctrlPts.count(), 1.0 );
209
210 return std::make_unique<QgsNurbsCurve>( ctrlPts, 3, knots, weights );
211}
212
213void QgsBezierData::clear()
214{
215 mData.clear();
216}
217
218int QgsBezierData::findClosestAnchor( const QgsPoint &point, double tolerance ) const
219{
220 int closestIndex = -1;
221 double minDistanceSquared = tolerance * tolerance;
222
223 for ( int i = 0; i < mData.count(); ++i )
224 {
225 const double dx = mData[i].anchor.x() - point.x();
226 const double dy = mData[i].anchor.y() - point.y();
227 const double distanceSquared = dx * dx + dy * dy;
228 if ( distanceSquared < minDistanceSquared )
229 {
230 minDistanceSquared = distanceSquared;
231 closestIndex = i;
232 }
233 }
234
235 return closestIndex;
236}
237
238int QgsBezierData::findClosestHandle( const QgsPoint &point, double tolerance ) const
239{
240 int closestIndex = -1;
241 double minDistanceSquared = tolerance * tolerance;
242
243 for ( int i = 0; i < mData.count(); ++i )
244 {
245 const QgsAnchorWithHandles &awh = mData[i];
246
247 // Check left handle (index 2*i)
248 if ( !qgsDoubleNear( awh.leftHandle.x(), awh.anchor.x() ) || !qgsDoubleNear( awh.leftHandle.y(), awh.anchor.y() ) )
249 {
250 const double dx = awh.leftHandle.x() - point.x();
251 const double dy = awh.leftHandle.y() - point.y();
252 const double distanceSquared = dx * dx + dy * dy;
253 if ( distanceSquared < minDistanceSquared )
254 {
255 minDistanceSquared = distanceSquared;
256 closestIndex = i * 2;
257 }
258 }
259
260 // Check right handle (index 2*i+1)
261 if ( !qgsDoubleNear( awh.rightHandle.x(), awh.anchor.x() ) || !qgsDoubleNear( awh.rightHandle.y(), awh.anchor.y() ) )
262 {
263 const double dx = awh.rightHandle.x() - point.x();
264 const double dy = awh.rightHandle.y() - point.y();
265 const double distanceSquared = dx * dx + dy * dy;
266 if ( distanceSquared < minDistanceSquared )
267 {
268 minDistanceSquared = distanceSquared;
269 closestIndex = i * 2 + 1;
270 }
271 }
272 }
273
274 return closestIndex;
275}
276
277int QgsBezierData::findClosestSegment( const QgsPoint &point, double tolerance ) const
278{
279 if ( mData.count() < 2 )
280 return -1;
281
282 int closestSegment = -1;
283 double minDistanceSquared = tolerance * tolerance;
284
285 // Check each segment
286 for ( int i = 0; i < mData.count() - 1; ++i )
287 {
288 const QgsPoint &p0 = mData[i].anchor;
289 const QgsPoint &p1 = mData[i].rightHandle;
290 const QgsPoint &p2 = mData[i + 1].leftHandle;
291 const QgsPoint &p3 = mData[i + 1].anchor;
292
293 // Sample the curve and find minimum distance
294 for ( int j = 0; j <= INTERPOLATION_POINTS; ++j )
295 {
296 const double t = static_cast<double>( j ) / INTERPOLATION_POINTS;
297 const QgsPoint curvePoint = QgsGeometryUtils::interpolatePointOnCubicBezier( p0, p1, p2, p3, t );
298
299 const double dx = curvePoint.x() - point.x();
300 const double dy = curvePoint.y() - point.y();
301 const double distanceSquared = dx * dx + dy * dy;
302
303 if ( distanceSquared < minDistanceSquared )
304 {
305 minDistanceSquared = distanceSquared;
306 closestSegment = i;
307 }
308 }
309 }
310
311 return closestSegment;
312}
313
bool is3D() const
Returns true if the geometry is 3D and contains a z-value.
static QgsPoint interpolatePointOnCubicBezier(const QgsPoint &p0, const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, double t)
Evaluates a point on a cubic Bézier curve defined by four control points.
static QVector< double > generateKnotsForBezierConversion(int nAnchors, int degree=3)
Generates a knot vector for converting piecewise Bézier curves to NURBS.
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:53
void setX(double x)
Sets the point's x-coordinate.
Definition qgspoint.h:359
double z
Definition qgspoint.h:58
double x
Definition qgspoint.h:56
double y
Definition qgspoint.h:57
bool qgsDoubleNear(double a, double b, double epsilon=4 *std::numeric_limits< double >::epsilon())
Compare two doubles (but allow some difference).
Definition qgis.h:6950
QVector< QgsPoint > QgsPointSequence
double closestSegment(const QgsPolylineXY &pl, const QgsPointXY &pt, int &vertexAfter, double epsilon)
Definition qgstracer.cpp:75