QGIS API Documentation 3.99.0-Master (18a1e75d814)
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( int degree ) const
187{
188 const int anchorCount = mData.count();
189 if ( anchorCount < 2 || degree < 1 )
190 return nullptr;
191
192 // Build control points
193 QVector<QgsPoint> ctrlPts;
194 ctrlPts.reserve( 1 + ( anchorCount - 1 ) * degree );
195 ctrlPts.append( mData[0].anchor );
196
197 for ( int i = 0; i < anchorCount - 1; ++i )
198 {
199 for ( int j = 1; j < degree; ++j )
200 {
201 if ( j == 1 )
202 {
203 ctrlPts.append( mData[i].rightHandle );
204 }
205 else if ( j == degree - 1 )
206 {
207 ctrlPts.append( mData[i + 1].leftHandle );
208 }
209 else
210 {
211 // degree > 3, we don't have intermediate handles.
212 // We'll just repeat the right handle as a fallback.
213 ctrlPts.append( mData[i].rightHandle );
214 }
215 }
216 ctrlPts.append( mData[i + 1].anchor );
217 }
218
219 QVector<double> knots = QgsNurbsCurve::generateKnotsForBezierConversion( anchorCount, degree );
220
221 // Uniform weights (non-rational B-spline)
222 QVector<double> weights( ctrlPts.count(), 1.0 );
223
224 return std::make_unique<QgsNurbsCurve>( ctrlPts, degree, knots, weights );
225}
226
227void QgsBezierData::clear()
228{
229 mData.clear();
230}
231
232int QgsBezierData::findClosestAnchor( const QgsPoint &point, double tolerance ) const
233{
234 int closestIndex = -1;
235 double minDistanceSquared = tolerance * tolerance;
236
237 for ( int i = 0; i < mData.count(); ++i )
238 {
239 const double dx = mData[i].anchor.x() - point.x();
240 const double dy = mData[i].anchor.y() - point.y();
241 const double distanceSquared = dx * dx + dy * dy;
242 if ( distanceSquared < minDistanceSquared )
243 {
244 minDistanceSquared = distanceSquared;
245 closestIndex = i;
246 }
247 }
248
249 return closestIndex;
250}
251
252int QgsBezierData::findClosestHandle( const QgsPoint &point, double tolerance ) const
253{
254 int closestIndex = -1;
255 double minDistanceSquared = tolerance * tolerance;
256
257 for ( int i = 0; i < mData.count(); ++i )
258 {
259 const QgsAnchorWithHandles &awh = mData[i];
260
261 // Check left handle (index 2*i)
262 if ( !qgsDoubleNear( awh.leftHandle.x(), awh.anchor.x() ) || !qgsDoubleNear( awh.leftHandle.y(), awh.anchor.y() ) )
263 {
264 const double dx = awh.leftHandle.x() - point.x();
265 const double dy = awh.leftHandle.y() - point.y();
266 const double distanceSquared = dx * dx + dy * dy;
267 if ( distanceSquared < minDistanceSquared )
268 {
269 minDistanceSquared = distanceSquared;
270 closestIndex = i * 2;
271 }
272 }
273
274 // Check right handle (index 2*i+1)
275 if ( !qgsDoubleNear( awh.rightHandle.x(), awh.anchor.x() ) || !qgsDoubleNear( awh.rightHandle.y(), awh.anchor.y() ) )
276 {
277 const double dx = awh.rightHandle.x() - point.x();
278 const double dy = awh.rightHandle.y() - point.y();
279 const double distanceSquared = dx * dx + dy * dy;
280 if ( distanceSquared < minDistanceSquared )
281 {
282 minDistanceSquared = distanceSquared;
283 closestIndex = i * 2 + 1;
284 }
285 }
286 }
287
288 return closestIndex;
289}
290
291int QgsBezierData::findClosestSegment( const QgsPoint &point, double tolerance ) const
292{
293 if ( mData.count() < 2 )
294 return -1;
295
296 int closestSegment = -1;
297 double minDistanceSquared = tolerance * tolerance;
298
299 // Check each segment
300 for ( int i = 0; i < mData.count() - 1; ++i )
301 {
302 const QgsPoint &p0 = mData[i].anchor;
303 const QgsPoint &p1 = mData[i].rightHandle;
304 const QgsPoint &p2 = mData[i + 1].leftHandle;
305 const QgsPoint &p3 = mData[i + 1].anchor;
306
307 // Sample the curve and find minimum distance
308 for ( int j = 0; j <= INTERPOLATION_POINTS; ++j )
309 {
310 const double t = static_cast<double>( j ) / INTERPOLATION_POINTS;
311 const QgsPoint curvePoint = QgsGeometryUtils::interpolatePointOnCubicBezier( p0, p1, p2, p3, t );
312
313 const double dx = curvePoint.x() - point.x();
314 const double dy = curvePoint.y() - point.y();
315 const double distanceSquared = dx * dx + dy * dy;
316
317 if ( distanceSquared < minDistanceSquared )
318 {
319 minDistanceSquared = distanceSquared;
320 closestSegment = i;
321 }
322 }
323 }
324
325 return closestSegment;
326}
327
328QgsBezierData QgsBezierData::fromPolyBezierControlPoints( const QVector<QgsPoint> &controlPoints, int degree )
329{
330 QgsBezierData data;
331
332 if ( degree < 1 )
333 return data;
334
335 const int n = controlPoints.size();
336 if ( n < degree + 1 || ( n - 1 ) % degree != 0 )
337 return data;
338
339 const int numAnchors = ( n - 1 ) / degree + 1;
340
341 for ( int i = 0; i < numAnchors; ++i )
342 {
343 const int anchorIndex = i * degree;
344 if ( anchorIndex >= n )
345 break;
346
347 const QgsPoint &anchor = controlPoints[anchorIndex];
348 QgsPoint leftHandle = anchor;
349 QgsPoint rightHandle = anchor;
350
351 if ( i > 0 )
352 {
353 const int leftIndex = anchorIndex - 1;
354 if ( leftIndex < n )
355 leftHandle = controlPoints[leftIndex];
356 }
357
358 if ( i < numAnchors - 1 )
359 {
360 const int rightIndex = anchorIndex + 1;
361 if ( rightIndex < n )
362 rightHandle = controlPoints[rightIndex];
363 }
364
365 data.addAnchor( anchor );
366 data.moveHandle( i * 2, leftHandle );
367 data.moveHandle( i * 2 + 1, rightHandle );
368 }
369
370 return data;
371}
372
373QgsBezierData QgsBezierData::fromPolyBezierControlPoints( const QVector<QgsPointXY> &controlPoints, int degree )
374{
375 QVector<QgsPoint> points;
376 points.reserve( controlPoints.size() );
377 for ( const QgsPointXY &pt : controlPoints )
378 points.append( QgsPoint( pt ) );
379 return fromPolyBezierControlPoints( points, degree );
380}
381
382void QgsBezierData::calculateSymmetricHandles( QVector<QgsPoint> &controlPoints, int anchorIndex, const QgsPoint &mousePosition )
383{
384 if ( anchorIndex < 0 || anchorIndex >= controlPoints.size() )
385 return;
386
387 QgsPoint *handleFollow = nullptr;
388 if ( anchorIndex + 1 < controlPoints.size() )
389 handleFollow = &controlPoints[anchorIndex + 1];
390
391 QgsPoint *handleOpposite = nullptr;
392 if ( anchorIndex - 1 >= 0 )
393 handleOpposite = &controlPoints[anchorIndex - 1];
394
395 calculateSymmetricHandles( controlPoints.at( anchorIndex ), mousePosition, handleFollow, handleOpposite );
396}
397
398void QgsBezierData::calculateSymmetricHandles( const QgsPoint &anchor, const QgsPoint &mousePosition, QgsPoint *handleFollow, QgsPoint *handleOpposite )
399{
400 // Calculate vector from anchor to mouse
401 const double dx = mousePosition.x() - anchor.x();
402 const double dy = mousePosition.y() - anchor.y();
403
404 if ( handleFollow )
405 {
406 handleFollow->setX( anchor.x() + dx );
407 handleFollow->setY( anchor.y() + dy );
408 }
409
410 if ( handleOpposite )
411 {
412 handleOpposite->setX( anchor.x() - dx );
413 handleOpposite->setY( anchor.y() - dy );
414 }
415}
416
417void QgsBezierData::calculateSymmetricHandles( int anchorIndex, const QgsPoint &mousePosition )
418{
419 if ( anchorIndex < 0 || anchorIndex >= mData.count() )
420 return;
421
422 QgsAnchorWithHandles &awh = mData[anchorIndex];
423 calculateSymmetricHandles( awh.anchor, mousePosition, &awh.rightHandle, &awh.leftHandle );
424}
425
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.
Represents a 2D point.
Definition qgspointxy.h:62
Point geometry type, with support for z-dimension and m-values.
Definition qgspoint.h:53
void setY(double y)
Sets the point's y-coordinate.
Definition qgspoint.h:370
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