diff --git a/images/images.qrc b/images/images.qrc
index 71f7568847de..521a86a1b671 100644
--- a/images/images.qrc
+++ b/images/images.qrc
@@ -294,6 +294,7 @@
themes/default/mActionDeselectActiveLayer.svg
themes/default/mActionDigitizeShape.svg
themes/default/mActionDigitizeWithCurve.svg
+ themes/default/mActionDigitizeWithNURBS.svg
themes/default/mActionDigitizeWithSegment.svg
themes/default/mActionDuplicateLayer.svg
themes/default/mActionDuplicateComposer.svg
@@ -744,6 +745,7 @@
themes/default/mIconSnappingAllLayers.svg
themes/default/mIconSnappingArea.svg
themes/default/mIconSnappingCentroid.svg
+ themes/default/mIconSnappingControlPoint.svg
themes/default/mIconSnappingMiddle.svg
themes/default/mIconSnappingOnScale.svg
themes/default/mIconSnappingVertex.svg
diff --git a/images/themes/default/mActionDigitizeWithNURBS.svg b/images/themes/default/mActionDigitizeWithNURBS.svg
new file mode 100644
index 000000000000..b9b84598f3bb
--- /dev/null
+++ b/images/themes/default/mActionDigitizeWithNURBS.svg
@@ -0,0 +1,82 @@
+
+
diff --git a/images/themes/default/mIconSnappingControlPoint.svg b/images/themes/default/mIconSnappingControlPoint.svg
new file mode 100644
index 000000000000..b9b84598f3bb
--- /dev/null
+++ b/images/themes/default/mIconSnappingControlPoint.svg
@@ -0,0 +1,82 @@
+
+
diff --git a/python/PyQt6/core/auto_additions/qgis.py b/python/PyQt6/core/auto_additions/qgis.py
index fab05889e1a1..fe9e4bf8155c 100644
--- a/python/PyQt6/core/auto_additions/qgis.py
+++ b/python/PyQt6/core/auto_additions/qgis.py
@@ -325,6 +325,9 @@
QgsWkbTypes.TIN = Qgis.WkbType.TIN
QgsWkbTypes.TIN.is_monkey_patched = True
QgsWkbTypes.TIN.__doc__ = "TIN \n.. versionadded:: 3.40"
+QgsWkbTypes.NurbsCurve = Qgis.WkbType.NurbsCurve
+QgsWkbTypes.NurbsCurve.is_monkey_patched = True
+QgsWkbTypes.NurbsCurve.__doc__ = "NurbsCurve \n.. versionadded:: 4.0"
QgsWkbTypes.NoGeometry = Qgis.WkbType.NoGeometry
QgsWkbTypes.NoGeometry.is_monkey_patched = True
QgsWkbTypes.NoGeometry.__doc__ = "No geometry"
@@ -373,6 +376,9 @@
QgsWkbTypes.TINZ = Qgis.WkbType.TINZ
QgsWkbTypes.TINZ.is_monkey_patched = True
QgsWkbTypes.TINZ.__doc__ = "TINZ"
+QgsWkbTypes.NurbsCurveZ = Qgis.WkbType.NurbsCurveZ
+QgsWkbTypes.NurbsCurveZ.is_monkey_patched = True
+QgsWkbTypes.NurbsCurveZ.__doc__ = "NurbsCurveZ \n.. versionadded:: 4.0"
QgsWkbTypes.PointM = Qgis.WkbType.PointM
QgsWkbTypes.PointM.is_monkey_patched = True
QgsWkbTypes.PointM.__doc__ = "PointM"
@@ -418,6 +424,9 @@
QgsWkbTypes.TINM = Qgis.WkbType.TINM
QgsWkbTypes.TINM.is_monkey_patched = True
QgsWkbTypes.TINM.__doc__ = "TINM"
+QgsWkbTypes.NurbsCurveM = Qgis.WkbType.NurbsCurveM
+QgsWkbTypes.NurbsCurveM.is_monkey_patched = True
+QgsWkbTypes.NurbsCurveM.__doc__ = "NurbsCurveM \n.. versionadded:: 4.0"
QgsWkbTypes.PointZM = Qgis.WkbType.PointZM
QgsWkbTypes.PointZM.is_monkey_patched = True
QgsWkbTypes.PointZM.__doc__ = "PointZM"
@@ -463,6 +472,9 @@
QgsWkbTypes.TriangleZM = Qgis.WkbType.TriangleZM
QgsWkbTypes.TriangleZM.is_monkey_patched = True
QgsWkbTypes.TriangleZM.__doc__ = "TriangleZM"
+QgsWkbTypes.NurbsCurveZM = Qgis.WkbType.NurbsCurveZM
+QgsWkbTypes.NurbsCurveZM.is_monkey_patched = True
+QgsWkbTypes.NurbsCurveZM.__doc__ = "NurbsCurveZM \n.. versionadded:: 4.0"
QgsWkbTypes.Point25D = Qgis.WkbType.Point25D
QgsWkbTypes.Point25D.is_monkey_patched = True
QgsWkbTypes.Point25D.__doc__ = "Point25D"
@@ -531,6 +543,10 @@
.. versionadded:: 3.40
+* ``NurbsCurve``: NurbsCurve
+
+ .. versionadded:: 4.0
+
* ``NoGeometry``: No geometry
* ``PointZ``: PointZ
* ``LineStringZ``: LineStringZ
@@ -547,6 +563,10 @@
* ``MultiSurfaceZ``: MultiSurfaceZ
* ``PolyhedralSurfaceZ``: PolyhedralSurfaceZ
* ``TINZ``: TINZ
+* ``NurbsCurveZ``: NurbsCurveZ
+
+ .. versionadded:: 4.0
+
* ``PointM``: PointM
* ``LineStringM``: LineStringM
* ``PolygonM``: PolygonM
@@ -562,6 +582,10 @@
* ``MultiSurfaceM``: MultiSurfaceM
* ``PolyhedralSurfaceM``: PolyhedralSurfaceM
* ``TINM``: TINM
+* ``NurbsCurveM``: NurbsCurveM
+
+ .. versionadded:: 4.0
+
* ``PointZM``: PointZM
* ``LineStringZM``: LineStringZM
* ``PolygonZM``: PolygonZM
@@ -577,6 +601,10 @@
* ``PolyhedralSurfaceZM``: PolyhedralSurfaceM
* ``TINZM``: TINZM
* ``TriangleZM``: TriangleZM
+* ``NurbsCurveZM``: NurbsCurveZM
+
+ .. versionadded:: 4.0
+
* ``Point25D``: Point25D
* ``LineString25D``: LineString25D
* ``Polygon25D``: Polygon25D
@@ -715,6 +743,7 @@
Qgis.CaptureTechnique.CircularString.__doc__ = "Capture in circular strings"
Qgis.CaptureTechnique.Streaming.__doc__ = "Streaming points digitizing mode (points are automatically added as the mouse cursor moves)."
Qgis.CaptureTechnique.Shape.__doc__ = "Digitize shapes."
+Qgis.CaptureTechnique.NurbsCurve.__doc__ = "Digitizes NURBS curves with control points. \n.. versionadded:: 4.0"
Qgis.CaptureTechnique.__doc__ = """Capture technique.
.. versionadded:: 3.26
@@ -723,11 +752,28 @@
* ``CircularString``: Capture in circular strings
* ``Streaming``: Streaming points digitizing mode (points are automatically added as the mouse cursor moves).
* ``Shape``: Digitize shapes.
+* ``NurbsCurve``: Digitizes NURBS curves with control points.
+
+ .. versionadded:: 4.0
+
"""
# --
Qgis.CaptureTechnique.baseClass = Qgis
# monkey patching scoped based enum
+Qgis.NurbsMode.ControlPoints.__doc__ = "Direct control points mode - the curve is attracted to control points but does not pass through them"
+Qgis.NurbsMode.PolyBezier.__doc__ = "Poly-Bézier mode (vector graphics style) - anchors with tangent handles, the curve passes through anchor points"
+Qgis.NurbsMode.__doc__ = """NURBS digitizing mode.
+
+.. versionadded:: 4.0
+
+* ``ControlPoints``: Direct control points mode - the curve is attracted to control points but does not pass through them
+* ``PolyBezier``: Poly-Bézier mode (vector graphics style) - anchors with tangent handles, the curve passes through anchor points
+
+"""
+# --
+Qgis.NurbsMode.baseClass = Qgis
+# monkey patching scoped based enum
Qgis.VectorLayerTypeFlag.SqlQuery.__doc__ = "SQL query layer"
Qgis.VectorLayerTypeFlag.__doc__ = """Vector layer type flags.
@@ -1390,6 +1436,9 @@
QgsSnappingConfig.SnappingTypes.LineEndpointFlag = Qgis.SnappingType.LineEndpoint
QgsSnappingConfig.LineEndpointFlag.is_monkey_patched = True
QgsSnappingConfig.LineEndpointFlag.__doc__ = "Start or end points of lines, or first vertex in polygon rings only \n.. versionadded:: 3.20"
+QgsSnappingConfig.ControlPoint = Qgis.SnappingType.ControlPoint
+QgsSnappingConfig.ControlPoint.is_monkey_patched = True
+QgsSnappingConfig.ControlPoint.__doc__ = "On control points (for NURBS curves) \n.. versionadded:: 4.0"
Qgis.SnappingType.__doc__ = """SnappingTypeFlag defines on what object the snapping is performed
.. versionadded:: 3.26
@@ -1425,6 +1474,10 @@
Available as ``QgsSnappingConfig.LineEndpointFlag`` in older QGIS releases.
+* ``ControlPoint``: On control points (for NURBS curves)
+
+ .. versionadded:: 4.0
+
"""
# --
@@ -5672,6 +5725,10 @@
QgsVertexId.VertexType.CurveVertex = Qgis.VertexType.Curve
QgsVertexId.CurveVertex.is_monkey_patched = True
QgsVertexId.CurveVertex.__doc__ = "An intermediate point on a segment defining the curvature of the segment"
+QgsVertexId.ControlPointVertex = Qgis.VertexType.ControlPoint
+QgsVertexId.VertexType.ControlPointVertex = Qgis.VertexType.ControlPoint
+QgsVertexId.ControlPointVertex.is_monkey_patched = True
+QgsVertexId.ControlPointVertex.__doc__ = "A NURBS control point (does not lie on the curve) \n.. versionadded:: 4.0"
Qgis.VertexType.__doc__ = """Types of vertex.
.. versionadded:: 3.22
@@ -5684,6 +5741,13 @@
Available as ``QgsVertexId.CurveVertex`` in older QGIS releases.
+* ``ControlPoint``: A NURBS control point (does not lie on the curve)
+
+ .. versionadded:: 4.0
+
+
+ Available as ``QgsVertexId.ControlPointVertex`` in older QGIS releases.
+
"""
# --
diff --git a/python/PyQt6/core/auto_additions/qgsgeometryutils.py b/python/PyQt6/core/auto_additions/qgsgeometryutils.py
index 7f4ba4c07083..fed6212dce3b 100644
--- a/python/PyQt6/core/auto_additions/qgsgeometryutils.py
+++ b/python/PyQt6/core/auto_additions/qgsgeometryutils.py
@@ -14,6 +14,7 @@
QgsGeometryUtils.projectPointOnSegment = staticmethod(QgsGeometryUtils.projectPointOnSegment)
QgsGeometryUtils.leftOfLine = staticmethod(QgsGeometryUtils.leftOfLine)
QgsGeometryUtils.interpolatePointOnArc = staticmethod(QgsGeometryUtils.interpolatePointOnArc)
+ QgsGeometryUtils.interpolatePointOnCubicBezier = staticmethod(QgsGeometryUtils.interpolatePointOnCubicBezier)
QgsGeometryUtils.segmentMidPoint = staticmethod(QgsGeometryUtils.segmentMidPoint)
QgsGeometryUtils.segmentMidPointFromCenter = staticmethod(QgsGeometryUtils.segmentMidPointFromCenter)
QgsGeometryUtils.circleTangentDirection = staticmethod(QgsGeometryUtils.circleTangentDirection)
diff --git a/python/PyQt6/core/auto_additions/qgsnurbscurve.py b/python/PyQt6/core/auto_additions/qgsnurbscurve.py
new file mode 100644
index 000000000000..b81e753b4201
--- /dev/null
+++ b/python/PyQt6/core/auto_additions/qgsnurbscurve.py
@@ -0,0 +1,6 @@
+# The following has been generated automatically from src/core/geometry/qgsnurbscurve.h
+try:
+ QgsNurbsCurve.__overridden_methods__ = ['clone', 'isClosed', 'isClosed2D', 'curveToLine', 'draw', 'drawAsPolygon', 'endPoint', 'equals', 'indexOf', 'interpolatePoint', 'numPoints', 'pointAt', 'points', 'reversed', 'scroll', 'startPoint', 'sumUpArea', 'xAt', 'yAt', 'zAt', 'mAt', 'asQPolygonF', 'addToPainterPath', 'curveSubstring', 'length', 'segmentLength', 'distanceBetweenVertices', 'snappedToGrid', 'simplifyByDistance', 'removeDuplicateNodes', 'vertexAngle', 'swapXy', 'transform', 'createEmptyWithSameType', 'closestSegment', 'boundingBox', 'boundingBox3D', 'moveVertex', 'insertVertex', 'wkbSize', 'asWkb', 'asWkt', 'asGml2', 'asGml3', 'asKml', 'dimension', 'isEmpty', 'clear', 'boundingBoxIntersects', 'centroid', 'addZValue', 'addMValue', 'dropZValue', 'dropMValue', 'deleteVertex', 'fromWkb', 'fromWkt', 'fuzzyEqual', 'fuzzyDistanceEqual', 'geometryType', 'hasCurvedSegments', 'partCount', 'toCurveType', 'vertexAt', 'vertexCount', 'vertexNumberFromVertexId', 'isValid', 'clearCache', 'compareToSameClass', 'calculateBoundingBox3D']
+ QgsNurbsCurve.__group__ = ['geometry']
+except (NameError, AttributeError):
+ pass
diff --git a/python/PyQt6/core/auto_additions/qgsnurbsutils.py b/python/PyQt6/core/auto_additions/qgsnurbsutils.py
new file mode 100644
index 000000000000..bad23ffbf1ec
--- /dev/null
+++ b/python/PyQt6/core/auto_additions/qgsnurbsutils.py
@@ -0,0 +1,7 @@
+# The following has been generated automatically from src/core/geometry/qgsnurbsutils.h
+try:
+ QgsNurbsUtils.containsNurbsCurve = staticmethod(QgsNurbsUtils.containsNurbsCurve)
+ QgsNurbsUtils.findMutableNurbsCurveForVertex = staticmethod(QgsNurbsUtils.findMutableNurbsCurveForVertex)
+ QgsNurbsUtils.__group__ = ['geometry']
+except (NameError, AttributeError):
+ pass
diff --git a/python/PyQt6/core/auto_additions/qgspointlocator.py b/python/PyQt6/core/auto_additions/qgspointlocator.py
index 25ea3d5f5a1d..8e548701dff3 100644
--- a/python/PyQt6/core/auto_additions/qgspointlocator.py
+++ b/python/PyQt6/core/auto_additions/qgspointlocator.py
@@ -6,6 +6,7 @@
QgsPointLocator.Centroid = QgsPointLocator.Type.Centroid
QgsPointLocator.MiddleOfSegment = QgsPointLocator.Type.MiddleOfSegment
QgsPointLocator.LineEndpoint = QgsPointLocator.Type.LineEndpoint
+QgsPointLocator.ControlPoint = QgsPointLocator.Type.ControlPoint
QgsPointLocator.All = QgsPointLocator.Type.All
QgsPointLocator.Types = lambda flags=0: QgsPointLocator.Type(flags)
try:
diff --git a/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in
index 249a9074011c..9a761fbb796b 100644
--- a/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in
+++ b/python/PyQt6/core/auto_generated/geometry/qgsabstractgeometry.sip.in
@@ -46,6 +46,8 @@ Abstract base class for all geometries.
sipType = sipType_QgsCircularString;
else if ( qgsgeometry_cast( sipCpp ) != nullptr )
sipType = sipType_QgsCompoundCurve;
+ else if ( qgsgeometry_cast( sipCpp ) != nullptr )
+ sipType = sipType_QgsNurbsCurve;
else if ( qgsgeometry_cast( sipCpp ) != nullptr )
sipType = sipType_QgsTriangle;
else if ( qgsgeometry_cast( sipCpp ) != nullptr )
diff --git a/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils.sip.in
index ae1aab8fefb1..b2fd9c34d7c7 100644
--- a/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils.sip.in
+++ b/python/PyQt6/core/auto_generated/geometry/qgsgeometryutils.sip.in
@@ -214,6 +214,27 @@ Any z or m values present in the points will also be linearly
interpolated in the output.
.. versionadded:: 3.4
+%End
+
+ static QgsPoint interpolatePointOnCubicBezier( const QgsPoint &p0, const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, double t ) /HoldGIL/;
+%Docstring
+Evaluates a point on a cubic Bézier curve defined by four control
+points.
+
+:param p0: start point (the curve passes through this point)
+:param p1: first control point
+:param p2: second control point
+:param p3: end point (the curve passes through this point)
+:param t: parameter value between 0 and 1
+
+:return: the point on the Bézier curve at parameter ``t``
+
+Any Z or M values present in the input points will also be interpolated.
+
+The cubic Bézier formula is: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ +
+t³P₃
+
+.. versionadded:: 4.0
%End
static bool segmentMidPoint( const QgsPoint &p1, const QgsPoint &p2, QgsPoint &result /Out/, double radius, const QgsPoint &mousePos ) /HoldGIL/;
diff --git a/python/PyQt6/core/auto_generated/geometry/qgsnurbscurve.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsnurbscurve.sip.in
new file mode 100644
index 000000000000..2c0900638579
--- /dev/null
+++ b/python/PyQt6/core/auto_generated/geometry/qgsnurbscurve.sip.in
@@ -0,0 +1,317 @@
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/core/geometry/qgsnurbscurve.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.py again *
+ ************************************************************************/
+
+
+
+
+
+
+class QgsNurbsCurve : QgsCurve
+{
+%Docstring(signature="appended")
+Represents a NURBS (Non-Uniform Rational B-Spline) curve geometry in
+2D/3D.
+
+NURBS curves are a mathematical model commonly used in computer graphics
+for representing curves. They are parametric curves defined by control
+points, weights, knot vectors, and a degree.
+
+.. versionadded:: 4.0
+%End
+
+%TypeHeaderCode
+#include "qgsnurbscurve.h"
+%End
+ public:
+ QgsNurbsCurve();
+%Docstring
+Constructor for an empty NURBS curve geometry.
+%End
+
+ QgsNurbsCurve( const QVector &ctrlPoints, int degree, const QVector &knots, const QVector &weights );
+%Docstring
+Constructs a NURBS curve from control points, degree, knot vector and
+weights.
+
+:param ctrlPoints: control points defining the curve. The number of
+ control points must be strictly greater than
+ ``degree``
+:param degree: degree of the NURBS curve (must be >= 1, typically 1-3)
+:param knots: knot vector (must have size = control points count +
+ degree + 1, values must be non-decreasing)
+:param weights: weight vector for rational curves (same size as control
+ points)
+%End
+
+ virtual QgsCurve *clone() const /Factory/;
+
+
+ QgsPoint evaluate( double t ) const;
+%Docstring
+Evaluates the NURBS curve at parameter t ∈ [0,1]. Uses the Cox-de Boor
+algorithm for B-spline basis function evaluation.
+
+:param t: parameter value between 0 and 1
+
+:return: point on the curve at parameter t
+%End
+
+ bool isBezier() const;
+%Docstring
+Returns ``True`` if this curve represents a Bézier curve. A Bézier curve
+is a special case of NURBS with uniform weights and specific knot
+vector.
+%End
+
+ bool isBSpline() const;
+%Docstring
+Returns ``True`` if this curve represents a B-spline (non-rational
+NURBS).
+%End
+
+ bool isRational() const;
+%Docstring
+Returns ``True`` if this curve is rational (has non-uniform weights).
+%End
+
+ bool isPolyBezier() const;
+%Docstring
+Returns ``True`` if this curve represents a poly-Bézier curve. A
+poly-Bézier is a degree 3 NURBS with (n-1) divisible by 3, where n is
+the number of control points.
+%End
+
+ virtual bool isClosed() const /HoldGIL/;
+
+ virtual bool isClosed2D() const /HoldGIL/;
+
+
+ virtual QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const /Factory/;
+
+ virtual void draw( QPainter &p ) const;
+
+ virtual void drawAsPolygon( QPainter &p ) const;
+
+ virtual QgsPoint endPoint() const /HoldGIL/;
+
+ virtual bool equals( const QgsCurve &other ) const;
+
+ virtual int indexOf( const QgsPoint &point ) const;
+
+ virtual QgsPoint *interpolatePoint( double distance ) const /Factory/;
+
+ virtual int numPoints() const /HoldGIL/;
+
+ virtual bool pointAt( int node, QgsPoint &point /Out/, Qgis::VertexType &type /Out/ ) const;
+
+ virtual void points( QgsPointSequence &pts /Out/ ) const;
+
+ virtual QgsCurve *reversed() const /Factory/;
+
+ virtual void scroll( int firstVertexIndex );
+
+ virtual QgsPoint startPoint() const /HoldGIL/;
+
+ virtual void sumUpArea( double &sum /Out/ ) const;
+
+ virtual double xAt( int index ) const;
+
+ virtual double yAt( int index ) const;
+
+ virtual double zAt( int index ) const;
+
+ virtual double mAt( int index ) const;
+
+
+ virtual QPolygonF asQPolygonF() const;
+
+
+ virtual void addToPainterPath( QPainterPath &path ) const;
+
+ virtual QgsCurve *curveSubstring( double startDistance, double endDistance ) const /Factory/;
+
+ virtual double length() const /HoldGIL/;
+
+ virtual double segmentLength( QgsVertexId startVertex ) const;
+
+ virtual double distanceBetweenVertices( QgsVertexId fromVertex, QgsVertexId toVertex ) const;
+
+ virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/;
+
+ virtual QgsAbstractGeometry *simplifyByDistance( double tolerance ) const /Factory/;
+
+ virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );
+
+ virtual double vertexAngle( QgsVertexId vertex ) const;
+
+ virtual void swapXy();
+
+ virtual bool transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback = 0 );
+
+ virtual QgsAbstractGeometry *createEmptyWithSameType() const /Factory/;
+
+ virtual double closestSegment( const QgsPoint &pt, QgsPoint &segmentPt /Out/, QgsVertexId &vertexAfter /Out/, int *leftOf /Out/ = 0, double epsilon = 4 * DBL_EPSILON ) const;
+
+ virtual void transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d = Qgis::TransformDirection::Forward, bool transformZ = false );
+
+ virtual void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 );
+
+ virtual QgsRectangle boundingBox() const;
+
+ virtual QgsBox3D boundingBox3D() const;
+
+ virtual bool moveVertex( QgsVertexId position, const QgsPoint &newPos );
+
+ virtual bool insertVertex( QgsVertexId position, const QgsPoint &vertex );
+
+ virtual int wkbSize( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const;
+
+ virtual QByteArray asWkb( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const;
+
+ virtual QString asWkt( int precision = 17 ) const;
+
+ virtual QDomElement asGml2( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const;
+
+ virtual QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const;
+
+ virtual QString asKml( int precision = 17 ) const;
+
+ virtual int dimension() const /HoldGIL/;
+
+ virtual bool isEmpty() const /HoldGIL/;
+
+ virtual void clear();
+
+ virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/;
+
+ virtual bool boundingBoxIntersects( const QgsBox3D &box3d ) const /HoldGIL/;
+
+ virtual QgsPoint centroid() const;
+
+
+ virtual bool addZValue( double zValue = 0 );
+
+ virtual bool addMValue( double mValue = 0 );
+
+ virtual bool dropZValue();
+
+ virtual bool dropMValue();
+
+ virtual bool deleteVertex( QgsVertexId position );
+
+ virtual bool fromWkb( QgsConstWkbPtr &wkb );
+
+ virtual bool fromWkt( const QString &wkt );
+
+ virtual bool fuzzyEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/;
+
+ virtual bool fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/;
+
+ virtual QString geometryType() const /HoldGIL/;
+
+ virtual bool hasCurvedSegments() const /HoldGIL/;
+
+ virtual int partCount() const /HoldGIL/;
+
+ virtual QgsCurve *toCurveType() const;
+
+ virtual QgsPoint vertexAt( QgsVertexId id ) const;
+
+ virtual int vertexCount( int part = 0, int ring = 0 ) const /HoldGIL/;
+
+ virtual int vertexNumberFromVertexId( QgsVertexId id ) const;
+
+ virtual bool isValid( QString &error /Out/, Qgis::GeometryValidityFlags flags = Qgis::GeometryValidityFlags() ) const;
+
+
+ int degree() const /HoldGIL/;
+%Docstring
+Returns the degree of the NURBS curve.
+%End
+
+ void setDegree( int degree );
+%Docstring
+Sets the degree of the NURBS curve.
+
+:param degree: curve degree (typically 1-3)
+%End
+
+ const QVector &controlPoints() const /HoldGIL/;
+%Docstring
+Returns the control points of the NURBS curve.
+%End
+
+ void setControlPoints( const QVector &points );
+%Docstring
+Sets the control points of the NURBS curve.
+
+:param points: control points
+%End
+
+ const QVector &knots() const /HoldGIL/;
+%Docstring
+Returns the knot vector of the NURBS curve.
+%End
+
+ void setKnots( const QVector &knots );
+%Docstring
+Sets the knot vector of the NURBS curve.
+
+:param knots: knot vector (must have size = control points count +
+ degree + 1, values must be non-decreasing)
+%End
+
+ const QVector &weights() const /HoldGIL/;
+%Docstring
+Returns the weight vector of the NURBS curve.
+%End
+
+ void setWeights( const QVector &weights );
+%Docstring
+Sets the weight vector of the NURBS curve.
+
+:param weights: weight vector (same size as control points)
+%End
+
+ double weight( int index ) const /HoldGIL/;
+%Docstring
+Returns the weight at the specified control point ``index``. Returns 1.0
+if index is out of range.
+
+.. versionadded:: 4.0
+%End
+
+ bool setWeight( int index, double weight );
+%Docstring
+Sets the ``weight`` at the specified control point ``index``. Weight
+must be positive (> 0).
+
+:return: ``True`` if successful, ``False`` if index is out of range or
+ weight is invalid.
+
+.. versionadded:: 4.0
+%End
+
+
+
+ protected:
+ virtual void clearCache() const;
+
+ int compareToSameClass( const QgsAbstractGeometry *other ) const final;
+ virtual QgsBox3D calculateBoundingBox3D() const;
+
+
+};
+
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/core/geometry/qgsnurbscurve.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.py again *
+ ************************************************************************/
diff --git a/python/PyQt6/core/auto_generated/geometry/qgsnurbsutils.sip.in b/python/PyQt6/core/auto_generated/geometry/qgsnurbsutils.sip.in
new file mode 100644
index 000000000000..2cc5ec7b9572
--- /dev/null
+++ b/python/PyQt6/core/auto_generated/geometry/qgsnurbsutils.sip.in
@@ -0,0 +1,69 @@
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/core/geometry/qgsnurbsutils.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.py again *
+ ************************************************************************/
+
+
+
+
+
+class QgsNurbsUtils
+{
+%Docstring(signature="appended")
+Utility functions for working with NURBS curves.
+
+.. versionadded:: 4.0
+%End
+
+%TypeHeaderCode
+#include "qgsnurbsutils.h"
+%End
+ public:
+
+ static bool containsNurbsCurve( const QgsAbstractGeometry *geom );
+%Docstring
+Returns ``True`` if the ``geom`` contains a NURBS curve (recursively).
+%End
+
+ static const QgsNurbsCurve *extractNurbsCurve( const QgsAbstractGeometry *geom );
+%Docstring
+Extracts the first NURBS curve found in the ``geom`` (recursively).
+Returns ``None`` if no NURBS curve is found.
+%End
+
+ static const QgsNurbsCurve *findNurbsCurveForVertex(
+ const QgsAbstractGeometry *geom,
+ const QgsVertexId &vid,
+ int &localIndex /Out/ );
+%Docstring
+Finds the NURBS curve containing the vertex identified by ``vid``.
+
+Returns the NURBS curve and sets ``localIndex`` to the control point
+index within that curve. Returns ``None`` if the vertex is not part of a
+NURBS curve.
+%End
+
+ static QgsNurbsCurve *findMutableNurbsCurveForVertex(
+ QgsAbstractGeometry *geom,
+ const QgsVertexId &vid,
+ int &localIndex /Out/ );
+%Docstring
+Mutable version of :py:func:`~QgsNurbsUtils.findNurbsCurveForVertex`.
+
+Finds the NURBS curve containing the vertex identified by ``vid``.
+Returns the NURBS curve and sets ``localIndex`` to the control point
+index within that curve. Returns ``None`` if the vertex is not part of a
+NURBS curve.
+%End
+};
+
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/core/geometry/qgsnurbsutils.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.py again *
+ ************************************************************************/
diff --git a/python/PyQt6/core/auto_generated/qgis.sip.in b/python/PyQt6/core/auto_generated/qgis.sip.in
index 2bca7118098e..1e60000a69b0 100644
--- a/python/PyQt6/core/auto_generated/qgis.sip.in
+++ b/python/PyQt6/core/auto_generated/qgis.sip.in
@@ -191,6 +191,7 @@ The development version
MultiSurface,
PolyhedralSurface,
TIN,
+ NurbsCurve,
NoGeometry,
PointZ,
LineStringZ,
@@ -207,6 +208,7 @@ The development version
MultiSurfaceZ,
PolyhedralSurfaceZ,
TINZ,
+ NurbsCurveZ,
PointM,
LineStringM,
PolygonM,
@@ -222,6 +224,7 @@ The development version
MultiSurfaceM,
PolyhedralSurfaceM,
TINM,
+ NurbsCurveM,
PointZM,
LineStringZM,
PolygonZM,
@@ -237,6 +240,7 @@ The development version
PolyhedralSurfaceZM,
TINZM,
TriangleZM,
+ NurbsCurveZM,
Point25D,
LineString25D,
Polygon25D,
@@ -279,6 +283,13 @@ The development version
CircularString,
Streaming,
Shape,
+ NurbsCurve,
+ };
+
+ enum class NurbsMode /BaseType=IntEnum/
+ {
+ ControlPoints,
+ PolyBezier,
};
enum class VectorLayerTypeFlag /BaseType=IntFlag/
@@ -484,6 +495,7 @@ The development version
Centroid,
MiddleOfSegment,
LineEndpoint,
+ ControlPoint,
};
typedef QFlags SnappingTypes;
@@ -1773,6 +1785,7 @@ The development version
{
Segment,
Curve,
+ ControlPoint,
};
enum class MarkerShape /BaseType=IntEnum/
diff --git a/python/PyQt6/core/auto_generated/qgspointlocator.sip.in b/python/PyQt6/core/auto_generated/qgspointlocator.sip.in
index a8e733267332..f6e7b7403372 100644
--- a/python/PyQt6/core/auto_generated/qgspointlocator.sip.in
+++ b/python/PyQt6/core/auto_generated/qgspointlocator.sip.in
@@ -36,10 +36,7 @@ Works with one layer.
#include "qgspointlocator.h"
%End
public:
-
- explicit QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateReferenceSystem &destinationCrs = QgsCoordinateReferenceSystem(),
- const QgsCoordinateTransformContext &transformContext = QgsCoordinateTransformContext(),
- const QgsRectangle *extent = 0 );
+ explicit QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateReferenceSystem &destinationCrs = QgsCoordinateReferenceSystem(), const QgsCoordinateTransformContext &transformContext = QgsCoordinateTransformContext(), const QgsRectangle *extent = 0 );
%Docstring
Construct point locator for a ``layer``.
@@ -95,6 +92,7 @@ visible feature
Centroid,
MiddleOfSegment,
LineEndpoint,
+ ControlPoint,
All
};
@@ -163,6 +161,13 @@ Returns ``True`` if the Match is the middle of a segment
Returns ``True`` if the Match is a line endpoint (start or end vertex).
.. versionadded:: 3.20
+%End
+
+ bool hasControlPoint() const;
+%Docstring
+Returns ``True`` if the Match is a control point (for NURBS curves).
+
+.. versionadded:: 4.0
%End
double distance() const;
@@ -258,6 +263,16 @@ unwanted matches. This method is either blocking or non blocking
according to ``relaxed`` parameter passed
.. versionadded:: 3.20
+%End
+
+ Match nearestControlPoint( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
+%Docstring
+Find nearest control point (for NURBS curves) to the specified point -
+up to distance specified by tolerance Optional filter may discard
+unwanted matches. This method is either blocking or non blocking
+according to ``relaxed`` parameter passed
+
+.. versionadded:: 4.0
%End
Match nearestEdge( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
diff --git a/python/PyQt6/core/class_map.yaml b/python/PyQt6/core/class_map.yaml
index c6627b3a3f2d..e8c6f4583a8e 100644
--- a/python/PyQt6/core/class_map.yaml
+++ b/python/PyQt6/core/class_map.yaml
@@ -1235,8 +1235,8 @@ QgsArrowInferSchemaOptions.geometryColumnName: src/core/qgsarrowiterator.h#L123
QgsArrowInferSchemaOptions.setGeometryColumnName: src/core/qgsarrowiterator.h#L115
QgsArrowInferSchemaOptions: src/core/qgsarrowiterator.h#L103
QgsArrowIterator.QgsArrowIterator: src/core/qgsarrowiterator.h#L332
-QgsArrowIterator.inferSchema: src/core/qgsarrowiterator.h#L364
-QgsArrowIterator.inferSchema: src/core/qgsarrowiterator.h#L371
+QgsArrowIterator.inferSchema(QgsFeatureSource*): src/core/qgsarrowiterator.h#L364
+QgsArrowIterator.inferSchema(QgsFeatureSource*,QgsArrowInferSchemaOptions): src/core/qgsarrowiterator.h#L371
QgsArrowIterator.nextFeatures: src/core/qgsarrowiterator.h#L357
QgsArrowIterator.setSchema: src/core/qgsarrowiterator.h#L344
QgsArrowIterator.toArrayStream: src/core/qgsarrowiterator.h#L347
diff --git a/python/PyQt6/core/core_auto.sip b/python/PyQt6/core/core_auto.sip
index c231336bf606..25994f82a34f 100644
--- a/python/PyQt6/core/core_auto.sip
+++ b/python/PyQt6/core/core_auto.sip
@@ -360,6 +360,8 @@
%Include auto_generated/geometry/qgsmultipoint.sip
%Include auto_generated/geometry/qgsmultipolygon.sip
%Include auto_generated/geometry/qgsmultisurface.sip
+%Include auto_generated/geometry/qgsnurbscurve.sip
+%Include auto_generated/geometry/qgsnurbsutils.sip
%Include auto_generated/geometry/qgsorientedbox3d.sip
%Include auto_generated/geometry/qgspoint.sip
%Include auto_generated/geometry/qgspolygon.sip
diff --git a/python/PyQt6/gui/auto_additions/qgsmaptoolcapture.py b/python/PyQt6/gui/auto_additions/qgsmaptoolcapture.py
index 8019939267b6..8c8d10ffdb06 100644
--- a/python/PyQt6/gui/auto_additions/qgsmaptoolcapture.py
+++ b/python/PyQt6/gui/auto_additions/qgsmaptoolcapture.py
@@ -19,7 +19,7 @@ def _force_int(v): return int(v.value) if isinstance(v, Enum) else v
QgsMapToolCapture.Capability.__or__ = lambda flag1, flag2: QgsMapToolCapture.Capability(_force_int(flag1) | _force_int(flag2))
try:
QgsMapToolCapture.__virtual_methods__ = ['capabilities', 'supportsTechnique', 'geometryCaptured', 'pointCaptured', 'lineCaptured', 'polygonCaptured']
- QgsMapToolCapture.__overridden_methods__ = ['activate', 'deactivate', 'cadCanvasMoveEvent', 'cadCanvasReleaseEvent', 'keyPressEvent', 'clean']
+ QgsMapToolCapture.__overridden_methods__ = ['activate', 'deactivate', 'cadCanvasPressEvent', 'cadCanvasMoveEvent', 'cadCanvasReleaseEvent', 'keyPressEvent', 'keyReleaseEvent', 'wheelEvent', 'clean']
QgsMapToolCapture.__group__ = ['maptools']
except (NameError, AttributeError):
pass
diff --git a/python/PyQt6/gui/auto_additions/qgsmaptooledit.py b/python/PyQt6/gui/auto_additions/qgsmaptooledit.py
index 3a085ff4f455..f6c829b2a968 100644
--- a/python/PyQt6/gui/auto_additions/qgsmaptooledit.py
+++ b/python/PyQt6/gui/auto_additions/qgsmaptooledit.py
@@ -5,6 +5,7 @@
try:
QgsMapToolEdit.defaultZValue = staticmethod(QgsMapToolEdit.defaultZValue)
QgsMapToolEdit.defaultMValue = staticmethod(QgsMapToolEdit.defaultMValue)
+ QgsMapToolEdit.applyControlPolygonStyle = staticmethod(QgsMapToolEdit.applyControlPolygonStyle)
QgsMapToolEdit.digitizingStrokeColor = staticmethod(QgsMapToolEdit.digitizingStrokeColor)
QgsMapToolEdit.digitizingStrokeWidth = staticmethod(QgsMapToolEdit.digitizingStrokeWidth)
QgsMapToolEdit.digitizingFillColor = staticmethod(QgsMapToolEdit.digitizingFillColor)
diff --git a/python/PyQt6/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in b/python/PyQt6/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in
index 473578dab593..23df60cb0b81 100644
--- a/python/PyQt6/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in
+++ b/python/PyQt6/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in
@@ -12,6 +12,7 @@
+
class QgsMapToolCapture : QgsMapToolAdvancedDigitizing
{
%Docstring(signature="appended")
@@ -115,6 +116,8 @@ Gets the capture curve
Returns a list of matches for each point on the captureCurve.
%End
+ virtual void cadCanvasPressEvent( QgsMapMouseEvent *e );
+
virtual void cadCanvasMoveEvent( QgsMapMouseEvent *e );
virtual void cadCanvasReleaseEvent( QgsMapMouseEvent *e );
@@ -126,6 +129,26 @@ Returns a list of matches for each point on the captureCurve.
Intercept key events like Esc or Del to delete the last point
:param e: key event
+%End
+
+ virtual void keyReleaseEvent( QKeyEvent *e );
+
+%Docstring
+Intercept key release events for NURBS weight editing mode
+
+:param e: key event
+
+.. versionadded:: 4.0
+%End
+
+ virtual void wheelEvent( QWheelEvent *e );
+
+%Docstring
+Intercept wheel events for NURBS weight adjustment
+
+:param e: wheel event
+
+.. versionadded:: 4.0
%End
void deleteTempRubberBand();
diff --git a/python/PyQt6/gui/auto_generated/maptools/qgsmaptooledit.sip.in b/python/PyQt6/gui/auto_generated/maptools/qgsmaptooledit.sip.in
index 8fe1198a56bc..13e6cb66db6f 100644
--- a/python/PyQt6/gui/auto_generated/maptools/qgsmaptooledit.sip.in
+++ b/python/PyQt6/gui/auto_generated/maptools/qgsmaptooledit.sip.in
@@ -46,6 +46,16 @@ settings. The caller takes ownership of the returned object
``False``.
%End
+ static void applyControlPolygonStyle( QgsRubberBand *rubberBand );
+%Docstring
+Applies the control polygon style to a rubber band (for NURBS/Bézier
+visualization). Uses settings for color and width, with dash line style.
+
+:param rubberBand: the rubber band to style
+
+.. versionadded:: 4.0
+%End
+
protected:
static QColor digitizingStrokeColor();
%Docstring
diff --git a/python/core/auto_additions/qgis.py b/python/core/auto_additions/qgis.py
index cf43960a3aa9..e25762c130e3 100644
--- a/python/core/auto_additions/qgis.py
+++ b/python/core/auto_additions/qgis.py
@@ -316,6 +316,9 @@
QgsWkbTypes.TIN = Qgis.WkbType.TIN
QgsWkbTypes.TIN.is_monkey_patched = True
QgsWkbTypes.TIN.__doc__ = "TIN \n.. versionadded:: 3.40"
+QgsWkbTypes.NurbsCurve = Qgis.WkbType.NurbsCurve
+QgsWkbTypes.NurbsCurve.is_monkey_patched = True
+QgsWkbTypes.NurbsCurve.__doc__ = "NurbsCurve \n.. versionadded:: 4.0"
QgsWkbTypes.NoGeometry = Qgis.WkbType.NoGeometry
QgsWkbTypes.NoGeometry.is_monkey_patched = True
QgsWkbTypes.NoGeometry.__doc__ = "No geometry"
@@ -364,6 +367,9 @@
QgsWkbTypes.TINZ = Qgis.WkbType.TINZ
QgsWkbTypes.TINZ.is_monkey_patched = True
QgsWkbTypes.TINZ.__doc__ = "TINZ"
+QgsWkbTypes.NurbsCurveZ = Qgis.WkbType.NurbsCurveZ
+QgsWkbTypes.NurbsCurveZ.is_monkey_patched = True
+QgsWkbTypes.NurbsCurveZ.__doc__ = "NurbsCurveZ \n.. versionadded:: 4.0"
QgsWkbTypes.PointM = Qgis.WkbType.PointM
QgsWkbTypes.PointM.is_monkey_patched = True
QgsWkbTypes.PointM.__doc__ = "PointM"
@@ -409,6 +415,9 @@
QgsWkbTypes.TINM = Qgis.WkbType.TINM
QgsWkbTypes.TINM.is_monkey_patched = True
QgsWkbTypes.TINM.__doc__ = "TINM"
+QgsWkbTypes.NurbsCurveM = Qgis.WkbType.NurbsCurveM
+QgsWkbTypes.NurbsCurveM.is_monkey_patched = True
+QgsWkbTypes.NurbsCurveM.__doc__ = "NurbsCurveM \n.. versionadded:: 4.0"
QgsWkbTypes.PointZM = Qgis.WkbType.PointZM
QgsWkbTypes.PointZM.is_monkey_patched = True
QgsWkbTypes.PointZM.__doc__ = "PointZM"
@@ -454,6 +463,9 @@
QgsWkbTypes.TriangleZM = Qgis.WkbType.TriangleZM
QgsWkbTypes.TriangleZM.is_monkey_patched = True
QgsWkbTypes.TriangleZM.__doc__ = "TriangleZM"
+QgsWkbTypes.NurbsCurveZM = Qgis.WkbType.NurbsCurveZM
+QgsWkbTypes.NurbsCurveZM.is_monkey_patched = True
+QgsWkbTypes.NurbsCurveZM.__doc__ = "NurbsCurveZM \n.. versionadded:: 4.0"
QgsWkbTypes.Point25D = Qgis.WkbType.Point25D
QgsWkbTypes.Point25D.is_monkey_patched = True
QgsWkbTypes.Point25D.__doc__ = "Point25D"
@@ -522,6 +534,10 @@
.. versionadded:: 3.40
+* ``NurbsCurve``: NurbsCurve
+
+ .. versionadded:: 4.0
+
* ``NoGeometry``: No geometry
* ``PointZ``: PointZ
* ``LineStringZ``: LineStringZ
@@ -538,6 +554,10 @@
* ``MultiSurfaceZ``: MultiSurfaceZ
* ``PolyhedralSurfaceZ``: PolyhedralSurfaceZ
* ``TINZ``: TINZ
+* ``NurbsCurveZ``: NurbsCurveZ
+
+ .. versionadded:: 4.0
+
* ``PointM``: PointM
* ``LineStringM``: LineStringM
* ``PolygonM``: PolygonM
@@ -553,6 +573,10 @@
* ``MultiSurfaceM``: MultiSurfaceM
* ``PolyhedralSurfaceM``: PolyhedralSurfaceM
* ``TINM``: TINM
+* ``NurbsCurveM``: NurbsCurveM
+
+ .. versionadded:: 4.0
+
* ``PointZM``: PointZM
* ``LineStringZM``: LineStringZM
* ``PolygonZM``: PolygonZM
@@ -568,6 +592,10 @@
* ``PolyhedralSurfaceZM``: PolyhedralSurfaceM
* ``TINZM``: TINZM
* ``TriangleZM``: TriangleZM
+* ``NurbsCurveZM``: NurbsCurveZM
+
+ .. versionadded:: 4.0
+
* ``Point25D``: Point25D
* ``LineString25D``: LineString25D
* ``Polygon25D``: Polygon25D
@@ -706,6 +734,7 @@
Qgis.CaptureTechnique.CircularString.__doc__ = "Capture in circular strings"
Qgis.CaptureTechnique.Streaming.__doc__ = "Streaming points digitizing mode (points are automatically added as the mouse cursor moves)."
Qgis.CaptureTechnique.Shape.__doc__ = "Digitize shapes."
+Qgis.CaptureTechnique.NurbsCurve.__doc__ = "Digitizes NURBS curves with control points. \n.. versionadded:: 4.0"
Qgis.CaptureTechnique.__doc__ = """Capture technique.
.. versionadded:: 3.26
@@ -714,11 +743,28 @@
* ``CircularString``: Capture in circular strings
* ``Streaming``: Streaming points digitizing mode (points are automatically added as the mouse cursor moves).
* ``Shape``: Digitize shapes.
+* ``NurbsCurve``: Digitizes NURBS curves with control points.
+
+ .. versionadded:: 4.0
+
"""
# --
Qgis.CaptureTechnique.baseClass = Qgis
# monkey patching scoped based enum
+Qgis.NurbsMode.ControlPoints.__doc__ = "Direct control points mode - the curve is attracted to control points but does not pass through them"
+Qgis.NurbsMode.PolyBezier.__doc__ = "Poly-Bézier mode (vector graphics style) - anchors with tangent handles, the curve passes through anchor points"
+Qgis.NurbsMode.__doc__ = """NURBS digitizing mode.
+
+.. versionadded:: 4.0
+
+* ``ControlPoints``: Direct control points mode - the curve is attracted to control points but does not pass through them
+* ``PolyBezier``: Poly-Bézier mode (vector graphics style) - anchors with tangent handles, the curve passes through anchor points
+
+"""
+# --
+Qgis.NurbsMode.baseClass = Qgis
+# monkey patching scoped based enum
Qgis.VectorLayerTypeFlag.SqlQuery.__doc__ = "SQL query layer"
Qgis.VectorLayerTypeFlag.__doc__ = """Vector layer type flags.
@@ -1374,6 +1420,9 @@
QgsSnappingConfig.SnappingTypes.LineEndpointFlag = Qgis.SnappingType.LineEndpoint
QgsSnappingConfig.LineEndpointFlag.is_monkey_patched = True
QgsSnappingConfig.LineEndpointFlag.__doc__ = "Start or end points of lines, or first vertex in polygon rings only \n.. versionadded:: 3.20"
+QgsSnappingConfig.ControlPoint = Qgis.SnappingType.ControlPoint
+QgsSnappingConfig.ControlPoint.is_monkey_patched = True
+QgsSnappingConfig.ControlPoint.__doc__ = "On control points (for NURBS curves) \n.. versionadded:: 4.0"
Qgis.SnappingType.__doc__ = """SnappingTypeFlag defines on what object the snapping is performed
.. versionadded:: 3.26
@@ -1409,6 +1458,10 @@
Available as ``QgsSnappingConfig.LineEndpointFlag`` in older QGIS releases.
+* ``ControlPoint``: On control points (for NURBS curves)
+
+ .. versionadded:: 4.0
+
"""
# --
@@ -5615,6 +5668,10 @@
QgsVertexId.VertexType.CurveVertex = Qgis.VertexType.Curve
QgsVertexId.CurveVertex.is_monkey_patched = True
QgsVertexId.CurveVertex.__doc__ = "An intermediate point on a segment defining the curvature of the segment"
+QgsVertexId.ControlPointVertex = Qgis.VertexType.ControlPoint
+QgsVertexId.VertexType.ControlPointVertex = Qgis.VertexType.ControlPoint
+QgsVertexId.ControlPointVertex.is_monkey_patched = True
+QgsVertexId.ControlPointVertex.__doc__ = "A NURBS control point (does not lie on the curve) \n.. versionadded:: 4.0"
Qgis.VertexType.__doc__ = """Types of vertex.
.. versionadded:: 3.22
@@ -5627,6 +5684,13 @@
Available as ``QgsVertexId.CurveVertex`` in older QGIS releases.
+* ``ControlPoint``: A NURBS control point (does not lie on the curve)
+
+ .. versionadded:: 4.0
+
+
+ Available as ``QgsVertexId.ControlPointVertex`` in older QGIS releases.
+
"""
# --
diff --git a/python/core/auto_additions/qgsgeometryutils.py b/python/core/auto_additions/qgsgeometryutils.py
index 7f4ba4c07083..fed6212dce3b 100644
--- a/python/core/auto_additions/qgsgeometryutils.py
+++ b/python/core/auto_additions/qgsgeometryutils.py
@@ -14,6 +14,7 @@
QgsGeometryUtils.projectPointOnSegment = staticmethod(QgsGeometryUtils.projectPointOnSegment)
QgsGeometryUtils.leftOfLine = staticmethod(QgsGeometryUtils.leftOfLine)
QgsGeometryUtils.interpolatePointOnArc = staticmethod(QgsGeometryUtils.interpolatePointOnArc)
+ QgsGeometryUtils.interpolatePointOnCubicBezier = staticmethod(QgsGeometryUtils.interpolatePointOnCubicBezier)
QgsGeometryUtils.segmentMidPoint = staticmethod(QgsGeometryUtils.segmentMidPoint)
QgsGeometryUtils.segmentMidPointFromCenter = staticmethod(QgsGeometryUtils.segmentMidPointFromCenter)
QgsGeometryUtils.circleTangentDirection = staticmethod(QgsGeometryUtils.circleTangentDirection)
diff --git a/python/core/auto_additions/qgsnurbscurve.py b/python/core/auto_additions/qgsnurbscurve.py
new file mode 100644
index 000000000000..b81e753b4201
--- /dev/null
+++ b/python/core/auto_additions/qgsnurbscurve.py
@@ -0,0 +1,6 @@
+# The following has been generated automatically from src/core/geometry/qgsnurbscurve.h
+try:
+ QgsNurbsCurve.__overridden_methods__ = ['clone', 'isClosed', 'isClosed2D', 'curveToLine', 'draw', 'drawAsPolygon', 'endPoint', 'equals', 'indexOf', 'interpolatePoint', 'numPoints', 'pointAt', 'points', 'reversed', 'scroll', 'startPoint', 'sumUpArea', 'xAt', 'yAt', 'zAt', 'mAt', 'asQPolygonF', 'addToPainterPath', 'curveSubstring', 'length', 'segmentLength', 'distanceBetweenVertices', 'snappedToGrid', 'simplifyByDistance', 'removeDuplicateNodes', 'vertexAngle', 'swapXy', 'transform', 'createEmptyWithSameType', 'closestSegment', 'boundingBox', 'boundingBox3D', 'moveVertex', 'insertVertex', 'wkbSize', 'asWkb', 'asWkt', 'asGml2', 'asGml3', 'asKml', 'dimension', 'isEmpty', 'clear', 'boundingBoxIntersects', 'centroid', 'addZValue', 'addMValue', 'dropZValue', 'dropMValue', 'deleteVertex', 'fromWkb', 'fromWkt', 'fuzzyEqual', 'fuzzyDistanceEqual', 'geometryType', 'hasCurvedSegments', 'partCount', 'toCurveType', 'vertexAt', 'vertexCount', 'vertexNumberFromVertexId', 'isValid', 'clearCache', 'compareToSameClass', 'calculateBoundingBox3D']
+ QgsNurbsCurve.__group__ = ['geometry']
+except (NameError, AttributeError):
+ pass
diff --git a/python/core/auto_additions/qgsnurbsutils.py b/python/core/auto_additions/qgsnurbsutils.py
new file mode 100644
index 000000000000..bad23ffbf1ec
--- /dev/null
+++ b/python/core/auto_additions/qgsnurbsutils.py
@@ -0,0 +1,7 @@
+# The following has been generated automatically from src/core/geometry/qgsnurbsutils.h
+try:
+ QgsNurbsUtils.containsNurbsCurve = staticmethod(QgsNurbsUtils.containsNurbsCurve)
+ QgsNurbsUtils.findMutableNurbsCurveForVertex = staticmethod(QgsNurbsUtils.findMutableNurbsCurveForVertex)
+ QgsNurbsUtils.__group__ = ['geometry']
+except (NameError, AttributeError):
+ pass
diff --git a/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in b/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in
index b086bc656619..5ea1a88bfe63 100644
--- a/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in
+++ b/python/core/auto_generated/geometry/qgsabstractgeometry.sip.in
@@ -46,6 +46,8 @@ Abstract base class for all geometries.
sipType = sipType_QgsCircularString;
else if ( qgsgeometry_cast( sipCpp ) != nullptr )
sipType = sipType_QgsCompoundCurve;
+ else if ( qgsgeometry_cast( sipCpp ) != nullptr )
+ sipType = sipType_QgsNurbsCurve;
else if ( qgsgeometry_cast( sipCpp ) != nullptr )
sipType = sipType_QgsTriangle;
else if ( qgsgeometry_cast( sipCpp ) != nullptr )
diff --git a/python/core/auto_generated/geometry/qgsgeometryutils.sip.in b/python/core/auto_generated/geometry/qgsgeometryutils.sip.in
index 62db84abc85f..ff1093242f86 100644
--- a/python/core/auto_generated/geometry/qgsgeometryutils.sip.in
+++ b/python/core/auto_generated/geometry/qgsgeometryutils.sip.in
@@ -214,6 +214,27 @@ Any z or m values present in the points will also be linearly
interpolated in the output.
.. versionadded:: 3.4
+%End
+
+ static QgsPoint interpolatePointOnCubicBezier( const QgsPoint &p0, const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, double t ) /HoldGIL/;
+%Docstring
+Evaluates a point on a cubic Bézier curve defined by four control
+points.
+
+:param p0: start point (the curve passes through this point)
+:param p1: first control point
+:param p2: second control point
+:param p3: end point (the curve passes through this point)
+:param t: parameter value between 0 and 1
+
+:return: the point on the Bézier curve at parameter ``t``
+
+Any Z or M values present in the input points will also be interpolated.
+
+The cubic Bézier formula is: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ +
+t³P₃
+
+.. versionadded:: 4.0
%End
static bool segmentMidPoint( const QgsPoint &p1, const QgsPoint &p2, QgsPoint &result /Out/, double radius, const QgsPoint &mousePos ) /HoldGIL/;
diff --git a/python/core/auto_generated/geometry/qgsnurbscurve.sip.in b/python/core/auto_generated/geometry/qgsnurbscurve.sip.in
new file mode 100644
index 000000000000..6d4b3f5a0db4
--- /dev/null
+++ b/python/core/auto_generated/geometry/qgsnurbscurve.sip.in
@@ -0,0 +1,317 @@
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/core/geometry/qgsnurbscurve.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.py again *
+ ************************************************************************/
+
+
+
+
+
+
+class QgsNurbsCurve : QgsCurve
+{
+%Docstring(signature="appended")
+Represents a NURBS (Non-Uniform Rational B-Spline) curve geometry in
+2D/3D.
+
+NURBS curves are a mathematical model commonly used in computer graphics
+for representing curves. They are parametric curves defined by control
+points, weights, knot vectors, and a degree.
+
+.. versionadded:: 4.0
+%End
+
+%TypeHeaderCode
+#include "qgsnurbscurve.h"
+%End
+ public:
+ QgsNurbsCurve();
+%Docstring
+Constructor for an empty NURBS curve geometry.
+%End
+
+ QgsNurbsCurve( const QVector &ctrlPoints, int degree, const QVector &knots, const QVector &weights );
+%Docstring
+Constructs a NURBS curve from control points, degree, knot vector and
+weights.
+
+:param ctrlPoints: control points defining the curve. The number of
+ control points must be strictly greater than
+ ``degree``
+:param degree: degree of the NURBS curve (must be >= 1, typically 1-3)
+:param knots: knot vector (must have size = control points count +
+ degree + 1, values must be non-decreasing)
+:param weights: weight vector for rational curves (same size as control
+ points)
+%End
+
+ virtual QgsCurve *clone() const /Factory/;
+
+
+ QgsPoint evaluate( double t ) const;
+%Docstring
+Evaluates the NURBS curve at parameter t ∈ [0,1]. Uses the Cox-de Boor
+algorithm for B-spline basis function evaluation.
+
+:param t: parameter value between 0 and 1
+
+:return: point on the curve at parameter t
+%End
+
+ bool isBezier() const;
+%Docstring
+Returns ``True`` if this curve represents a Bézier curve. A Bézier curve
+is a special case of NURBS with uniform weights and specific knot
+vector.
+%End
+
+ bool isBSpline() const;
+%Docstring
+Returns ``True`` if this curve represents a B-spline (non-rational
+NURBS).
+%End
+
+ bool isRational() const;
+%Docstring
+Returns ``True`` if this curve is rational (has non-uniform weights).
+%End
+
+ bool isPolyBezier() const;
+%Docstring
+Returns ``True`` if this curve represents a poly-Bézier curve. A
+poly-Bézier is a degree 3 NURBS with (n-1) divisible by 3, where n is
+the number of control points.
+%End
+
+ virtual bool isClosed() const /HoldGIL/;
+
+ virtual bool isClosed2D() const /HoldGIL/;
+
+
+ virtual QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const /Factory/;
+
+ virtual void draw( QPainter &p ) const;
+
+ virtual void drawAsPolygon( QPainter &p ) const;
+
+ virtual QgsPoint endPoint() const /HoldGIL/;
+
+ virtual bool equals( const QgsCurve &other ) const;
+
+ virtual int indexOf( const QgsPoint &point ) const;
+
+ virtual QgsPoint *interpolatePoint( double distance ) const /Factory/;
+
+ virtual int numPoints() const /HoldGIL/;
+
+ virtual bool pointAt( int node, QgsPoint &point /Out/, Qgis::VertexType &type /Out/ ) const;
+
+ virtual void points( QgsPointSequence &pts /Out/ ) const;
+
+ virtual QgsCurve *reversed() const /Factory/;
+
+ virtual void scroll( int firstVertexIndex );
+
+ virtual QgsPoint startPoint() const /HoldGIL/;
+
+ virtual void sumUpArea( double &sum /Out/ ) const;
+
+ virtual double xAt( int index ) const;
+
+ virtual double yAt( int index ) const;
+
+ virtual double zAt( int index ) const;
+
+ virtual double mAt( int index ) const;
+
+
+ virtual QPolygonF asQPolygonF() const;
+
+
+ virtual void addToPainterPath( QPainterPath &path ) const;
+
+ virtual QgsCurve *curveSubstring( double startDistance, double endDistance ) const /Factory/;
+
+ virtual double length() const /HoldGIL/;
+
+ virtual double segmentLength( QgsVertexId startVertex ) const;
+
+ virtual double distanceBetweenVertices( QgsVertexId fromVertex, QgsVertexId toVertex ) const;
+
+ virtual QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const /Factory/;
+
+ virtual QgsAbstractGeometry *simplifyByDistance( double tolerance ) const /Factory/;
+
+ virtual bool removeDuplicateNodes( double epsilon = 4 * DBL_EPSILON, bool useZValues = false );
+
+ virtual double vertexAngle( QgsVertexId vertex ) const;
+
+ virtual void swapXy();
+
+ virtual bool transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback = 0 );
+
+ virtual QgsAbstractGeometry *createEmptyWithSameType() const /Factory/;
+
+ virtual double closestSegment( const QgsPoint &pt, QgsPoint &segmentPt /Out/, QgsVertexId &vertexAfter /Out/, int *leftOf /Out/ = 0, double epsilon = 4 * DBL_EPSILON ) const;
+
+ virtual void transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d = Qgis::TransformDirection::Forward, bool transformZ = false ) throw( QgsCsException );
+
+ virtual void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 );
+
+ virtual QgsRectangle boundingBox() const;
+
+ virtual QgsBox3D boundingBox3D() const;
+
+ virtual bool moveVertex( QgsVertexId position, const QgsPoint &newPos );
+
+ virtual bool insertVertex( QgsVertexId position, const QgsPoint &vertex );
+
+ virtual int wkbSize( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const;
+
+ virtual QByteArray asWkb( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const;
+
+ virtual QString asWkt( int precision = 17 ) const;
+
+ virtual QDomElement asGml2( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const;
+
+ virtual QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const;
+
+ virtual QString asKml( int precision = 17 ) const;
+
+ virtual int dimension() const /HoldGIL/;
+
+ virtual bool isEmpty() const /HoldGIL/;
+
+ virtual void clear();
+
+ virtual bool boundingBoxIntersects( const QgsRectangle &rectangle ) const /HoldGIL/;
+
+ virtual bool boundingBoxIntersects( const QgsBox3D &box3d ) const /HoldGIL/;
+
+ virtual QgsPoint centroid() const;
+
+
+ virtual bool addZValue( double zValue = 0 );
+
+ virtual bool addMValue( double mValue = 0 );
+
+ virtual bool dropZValue();
+
+ virtual bool dropMValue();
+
+ virtual bool deleteVertex( QgsVertexId position );
+
+ virtual bool fromWkb( QgsConstWkbPtr &wkb );
+
+ virtual bool fromWkt( const QString &wkt );
+
+ virtual bool fuzzyEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/;
+
+ virtual bool fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const /HoldGIL/;
+
+ virtual QString geometryType() const /HoldGIL/;
+
+ virtual bool hasCurvedSegments() const /HoldGIL/;
+
+ virtual int partCount() const /HoldGIL/;
+
+ virtual QgsCurve *toCurveType() const;
+
+ virtual QgsPoint vertexAt( QgsVertexId id ) const;
+
+ virtual int vertexCount( int part = 0, int ring = 0 ) const /HoldGIL/;
+
+ virtual int vertexNumberFromVertexId( QgsVertexId id ) const;
+
+ virtual bool isValid( QString &error /Out/, Qgis::GeometryValidityFlags flags = Qgis::GeometryValidityFlags() ) const;
+
+
+ int degree() const /HoldGIL/;
+%Docstring
+Returns the degree of the NURBS curve.
+%End
+
+ void setDegree( int degree );
+%Docstring
+Sets the degree of the NURBS curve.
+
+:param degree: curve degree (typically 1-3)
+%End
+
+ const QVector &controlPoints() const /HoldGIL/;
+%Docstring
+Returns the control points of the NURBS curve.
+%End
+
+ void setControlPoints( const QVector &points );
+%Docstring
+Sets the control points of the NURBS curve.
+
+:param points: control points
+%End
+
+ const QVector &knots() const /HoldGIL/;
+%Docstring
+Returns the knot vector of the NURBS curve.
+%End
+
+ void setKnots( const QVector &knots );
+%Docstring
+Sets the knot vector of the NURBS curve.
+
+:param knots: knot vector (must have size = control points count +
+ degree + 1, values must be non-decreasing)
+%End
+
+ const QVector &weights() const /HoldGIL/;
+%Docstring
+Returns the weight vector of the NURBS curve.
+%End
+
+ void setWeights( const QVector &weights );
+%Docstring
+Sets the weight vector of the NURBS curve.
+
+:param weights: weight vector (same size as control points)
+%End
+
+ double weight( int index ) const /HoldGIL/;
+%Docstring
+Returns the weight at the specified control point ``index``. Returns 1.0
+if index is out of range.
+
+.. versionadded:: 4.0
+%End
+
+ bool setWeight( int index, double weight );
+%Docstring
+Sets the ``weight`` at the specified control point ``index``. Weight
+must be positive (> 0).
+
+:return: ``True`` if successful, ``False`` if index is out of range or
+ weight is invalid.
+
+.. versionadded:: 4.0
+%End
+
+
+
+ protected:
+ virtual void clearCache() const;
+
+ int compareToSameClass( const QgsAbstractGeometry *other ) const final;
+ virtual QgsBox3D calculateBoundingBox3D() const;
+
+
+};
+
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/core/geometry/qgsnurbscurve.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.py again *
+ ************************************************************************/
diff --git a/python/core/auto_generated/geometry/qgsnurbsutils.sip.in b/python/core/auto_generated/geometry/qgsnurbsutils.sip.in
new file mode 100644
index 000000000000..2cc5ec7b9572
--- /dev/null
+++ b/python/core/auto_generated/geometry/qgsnurbsutils.sip.in
@@ -0,0 +1,69 @@
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/core/geometry/qgsnurbsutils.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.py again *
+ ************************************************************************/
+
+
+
+
+
+class QgsNurbsUtils
+{
+%Docstring(signature="appended")
+Utility functions for working with NURBS curves.
+
+.. versionadded:: 4.0
+%End
+
+%TypeHeaderCode
+#include "qgsnurbsutils.h"
+%End
+ public:
+
+ static bool containsNurbsCurve( const QgsAbstractGeometry *geom );
+%Docstring
+Returns ``True`` if the ``geom`` contains a NURBS curve (recursively).
+%End
+
+ static const QgsNurbsCurve *extractNurbsCurve( const QgsAbstractGeometry *geom );
+%Docstring
+Extracts the first NURBS curve found in the ``geom`` (recursively).
+Returns ``None`` if no NURBS curve is found.
+%End
+
+ static const QgsNurbsCurve *findNurbsCurveForVertex(
+ const QgsAbstractGeometry *geom,
+ const QgsVertexId &vid,
+ int &localIndex /Out/ );
+%Docstring
+Finds the NURBS curve containing the vertex identified by ``vid``.
+
+Returns the NURBS curve and sets ``localIndex`` to the control point
+index within that curve. Returns ``None`` if the vertex is not part of a
+NURBS curve.
+%End
+
+ static QgsNurbsCurve *findMutableNurbsCurveForVertex(
+ QgsAbstractGeometry *geom,
+ const QgsVertexId &vid,
+ int &localIndex /Out/ );
+%Docstring
+Mutable version of :py:func:`~QgsNurbsUtils.findNurbsCurveForVertex`.
+
+Finds the NURBS curve containing the vertex identified by ``vid``.
+Returns the NURBS curve and sets ``localIndex`` to the control point
+index within that curve. Returns ``None`` if the vertex is not part of a
+NURBS curve.
+%End
+};
+
+/************************************************************************
+ * This file has been generated automatically from *
+ * *
+ * src/core/geometry/qgsnurbsutils.h *
+ * *
+ * Do not edit manually ! Edit header and run scripts/sipify.py again *
+ ************************************************************************/
diff --git a/python/core/auto_generated/qgis.sip.in b/python/core/auto_generated/qgis.sip.in
index 6326d88b6639..efa212ed01df 100644
--- a/python/core/auto_generated/qgis.sip.in
+++ b/python/core/auto_generated/qgis.sip.in
@@ -191,6 +191,7 @@ The development version
MultiSurface,
PolyhedralSurface,
TIN,
+ NurbsCurve,
NoGeometry,
PointZ,
LineStringZ,
@@ -207,6 +208,7 @@ The development version
MultiSurfaceZ,
PolyhedralSurfaceZ,
TINZ,
+ NurbsCurveZ,
PointM,
LineStringM,
PolygonM,
@@ -222,6 +224,7 @@ The development version
MultiSurfaceM,
PolyhedralSurfaceM,
TINM,
+ NurbsCurveM,
PointZM,
LineStringZM,
PolygonZM,
@@ -237,6 +240,7 @@ The development version
PolyhedralSurfaceZM,
TINZM,
TriangleZM,
+ NurbsCurveZM,
Point25D,
LineString25D,
Polygon25D,
@@ -279,6 +283,13 @@ The development version
CircularString,
Streaming,
Shape,
+ NurbsCurve,
+ };
+
+ enum class NurbsMode
+ {
+ ControlPoints,
+ PolyBezier,
};
enum class VectorLayerTypeFlag
@@ -484,6 +495,7 @@ The development version
Centroid,
MiddleOfSegment,
LineEndpoint,
+ ControlPoint,
};
typedef QFlags SnappingTypes;
@@ -1773,6 +1785,7 @@ The development version
{
Segment,
Curve,
+ ControlPoint,
};
enum class MarkerShape
diff --git a/python/core/auto_generated/qgspointlocator.sip.in b/python/core/auto_generated/qgspointlocator.sip.in
index e5c6b66721bf..f115937352b6 100644
--- a/python/core/auto_generated/qgspointlocator.sip.in
+++ b/python/core/auto_generated/qgspointlocator.sip.in
@@ -36,10 +36,7 @@ Works with one layer.
#include "qgspointlocator.h"
%End
public:
-
- explicit QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateReferenceSystem &destinationCrs = QgsCoordinateReferenceSystem(),
- const QgsCoordinateTransformContext &transformContext = QgsCoordinateTransformContext(),
- const QgsRectangle *extent = 0 );
+ explicit QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateReferenceSystem &destinationCrs = QgsCoordinateReferenceSystem(), const QgsCoordinateTransformContext &transformContext = QgsCoordinateTransformContext(), const QgsRectangle *extent = 0 );
%Docstring
Construct point locator for a ``layer``.
@@ -95,6 +92,7 @@ visible feature
Centroid,
MiddleOfSegment,
LineEndpoint,
+ ControlPoint,
All
};
@@ -163,6 +161,13 @@ Returns ``True`` if the Match is the middle of a segment
Returns ``True`` if the Match is a line endpoint (start or end vertex).
.. versionadded:: 3.20
+%End
+
+ bool hasControlPoint() const;
+%Docstring
+Returns ``True`` if the Match is a control point (for NURBS curves).
+
+.. versionadded:: 4.0
%End
double distance() const;
@@ -258,6 +263,16 @@ unwanted matches. This method is either blocking or non blocking
according to ``relaxed`` parameter passed
.. versionadded:: 3.20
+%End
+
+ Match nearestControlPoint( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
+%Docstring
+Find nearest control point (for NURBS curves) to the specified point -
+up to distance specified by tolerance Optional filter may discard
+unwanted matches. This method is either blocking or non blocking
+according to ``relaxed`` parameter passed
+
+.. versionadded:: 4.0
%End
Match nearestEdge( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = 0, bool relaxed = false );
diff --git a/python/core/class_map.yaml b/python/core/class_map.yaml
index 538e9de512e4..5353e795d8cc 100644
--- a/python/core/class_map.yaml
+++ b/python/core/class_map.yaml
@@ -1235,8 +1235,8 @@ QgsArrowInferSchemaOptions.geometryColumnName: src/core/qgsarrowiterator.h#L123
QgsArrowInferSchemaOptions.setGeometryColumnName: src/core/qgsarrowiterator.h#L115
QgsArrowInferSchemaOptions: src/core/qgsarrowiterator.h#L103
QgsArrowIterator.QgsArrowIterator: src/core/qgsarrowiterator.h#L332
-QgsArrowIterator.inferSchema: src/core/qgsarrowiterator.h#L364
-QgsArrowIterator.inferSchema: src/core/qgsarrowiterator.h#L371
+QgsArrowIterator.inferSchema(QgsFeatureSource*): src/core/qgsarrowiterator.h#L364
+QgsArrowIterator.inferSchema(QgsFeatureSource*,QgsArrowInferSchemaOptions): src/core/qgsarrowiterator.h#L371
QgsArrowIterator.nextFeatures: src/core/qgsarrowiterator.h#L357
QgsArrowIterator.setSchema: src/core/qgsarrowiterator.h#L344
QgsArrowIterator.toArrayStream: src/core/qgsarrowiterator.h#L347
diff --git a/python/core/core_auto.sip b/python/core/core_auto.sip
index c231336bf606..25994f82a34f 100644
--- a/python/core/core_auto.sip
+++ b/python/core/core_auto.sip
@@ -360,6 +360,8 @@
%Include auto_generated/geometry/qgsmultipoint.sip
%Include auto_generated/geometry/qgsmultipolygon.sip
%Include auto_generated/geometry/qgsmultisurface.sip
+%Include auto_generated/geometry/qgsnurbscurve.sip
+%Include auto_generated/geometry/qgsnurbsutils.sip
%Include auto_generated/geometry/qgsorientedbox3d.sip
%Include auto_generated/geometry/qgspoint.sip
%Include auto_generated/geometry/qgspolygon.sip
diff --git a/python/gui/auto_additions/qgsmaptoolcapture.py b/python/gui/auto_additions/qgsmaptoolcapture.py
index 536fade9a9ba..f76cc24dee5d 100644
--- a/python/gui/auto_additions/qgsmaptoolcapture.py
+++ b/python/gui/auto_additions/qgsmaptoolcapture.py
@@ -1,7 +1,7 @@
# The following has been generated automatically from src/gui/maptools/qgsmaptoolcapture.h
try:
QgsMapToolCapture.__virtual_methods__ = ['capabilities', 'supportsTechnique', 'geometryCaptured', 'pointCaptured', 'lineCaptured', 'polygonCaptured']
- QgsMapToolCapture.__overridden_methods__ = ['activate', 'deactivate', 'cadCanvasMoveEvent', 'cadCanvasReleaseEvent', 'keyPressEvent', 'clean']
+ QgsMapToolCapture.__overridden_methods__ = ['activate', 'deactivate', 'cadCanvasPressEvent', 'cadCanvasMoveEvent', 'cadCanvasReleaseEvent', 'keyPressEvent', 'keyReleaseEvent', 'wheelEvent', 'clean']
QgsMapToolCapture.__group__ = ['maptools']
except (NameError, AttributeError):
pass
diff --git a/python/gui/auto_additions/qgsmaptooledit.py b/python/gui/auto_additions/qgsmaptooledit.py
index 224aa9fcd313..0cd1db49674b 100644
--- a/python/gui/auto_additions/qgsmaptooledit.py
+++ b/python/gui/auto_additions/qgsmaptooledit.py
@@ -2,6 +2,7 @@
try:
QgsMapToolEdit.defaultZValue = staticmethod(QgsMapToolEdit.defaultZValue)
QgsMapToolEdit.defaultMValue = staticmethod(QgsMapToolEdit.defaultMValue)
+ QgsMapToolEdit.applyControlPolygonStyle = staticmethod(QgsMapToolEdit.applyControlPolygonStyle)
QgsMapToolEdit.digitizingStrokeColor = staticmethod(QgsMapToolEdit.digitizingStrokeColor)
QgsMapToolEdit.digitizingStrokeWidth = staticmethod(QgsMapToolEdit.digitizingStrokeWidth)
QgsMapToolEdit.digitizingFillColor = staticmethod(QgsMapToolEdit.digitizingFillColor)
diff --git a/python/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in b/python/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in
index e4f8ff3b09c1..b31217b2cd73 100644
--- a/python/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in
+++ b/python/gui/auto_generated/maptools/qgsmaptoolcapture.sip.in
@@ -12,6 +12,7 @@
+
class QgsMapToolCapture : QgsMapToolAdvancedDigitizing
{
%Docstring(signature="appended")
@@ -115,6 +116,8 @@ Gets the capture curve
Returns a list of matches for each point on the captureCurve.
%End
+ virtual void cadCanvasPressEvent( QgsMapMouseEvent *e );
+
virtual void cadCanvasMoveEvent( QgsMapMouseEvent *e );
virtual void cadCanvasReleaseEvent( QgsMapMouseEvent *e );
@@ -126,6 +129,26 @@ Returns a list of matches for each point on the captureCurve.
Intercept key events like Esc or Del to delete the last point
:param e: key event
+%End
+
+ virtual void keyReleaseEvent( QKeyEvent *e );
+
+%Docstring
+Intercept key release events for NURBS weight editing mode
+
+:param e: key event
+
+.. versionadded:: 4.0
+%End
+
+ virtual void wheelEvent( QWheelEvent *e );
+
+%Docstring
+Intercept wheel events for NURBS weight adjustment
+
+:param e: wheel event
+
+.. versionadded:: 4.0
%End
void deleteTempRubberBand();
diff --git a/python/gui/auto_generated/maptools/qgsmaptooledit.sip.in b/python/gui/auto_generated/maptools/qgsmaptooledit.sip.in
index 514f9664aec3..2557a45c3751 100644
--- a/python/gui/auto_generated/maptools/qgsmaptooledit.sip.in
+++ b/python/gui/auto_generated/maptools/qgsmaptooledit.sip.in
@@ -46,6 +46,16 @@ settings. The caller takes ownership of the returned object
``False``.
%End
+ static void applyControlPolygonStyle( QgsRubberBand *rubberBand );
+%Docstring
+Applies the control polygon style to a rubber band (for NURBS/Bézier
+visualization). Uses settings for color and width, with dash line style.
+
+:param rubberBand: the rubber band to style
+
+.. versionadded:: 4.0
+%End
+
protected:
static QColor digitizingStrokeColor();
%Docstring
diff --git a/src/app/elevation/qgsmaptoolprofilecurve.cpp b/src/app/elevation/qgsmaptoolprofilecurve.cpp
index e69f627fcd1f..25530c1a3fcd 100644
--- a/src/app/elevation/qgsmaptoolprofilecurve.cpp
+++ b/src/app/elevation/qgsmaptoolprofilecurve.cpp
@@ -46,6 +46,7 @@ bool QgsMapToolProfileCurve::supportsTechnique( Qgis::CaptureTechnique technique
case Qgis::CaptureTechnique::StraightSegments:
case Qgis::CaptureTechnique::CircularString:
case Qgis::CaptureTechnique::Streaming:
+ case Qgis::CaptureTechnique::NurbsCurve:
return true;
case Qgis::CaptureTechnique::Shape:
diff --git a/src/app/maptools/qgsmaptoolsdigitizingtechniquemanager.cpp b/src/app/maptools/qgsmaptoolsdigitizingtechniquemanager.cpp
index 9ebb7fdae693..4a02524cd84b 100644
--- a/src/app/maptools/qgsmaptoolsdigitizingtechniquemanager.cpp
+++ b/src/app/maptools/qgsmaptoolsdigitizingtechniquemanager.cpp
@@ -30,6 +30,7 @@
#include
#include
+#include
#include
#include
@@ -46,6 +47,7 @@ QgsMapToolsDigitizingTechniqueManager::QgsMapToolsDigitizingTechniqueManager( QO
mTechniqueActions.insert( Qgis::CaptureTechnique::CircularString, QgisApp::instance()->mActionDigitizeWithCurve );
mTechniqueActions.insert( Qgis::CaptureTechnique::Streaming, QgisApp::instance()->mActionStreamDigitize );
mTechniqueActions.insert( Qgis::CaptureTechnique::Shape, QgisApp::instance()->mActionDigitizeShape );
+ mTechniqueActions.insert( Qgis::CaptureTechnique::NurbsCurve, QgisApp::instance()->mActionDigitizeWithNurbs );
mDigitizeModeToolButton = new QToolButton();
mDigitizeModeToolButton->setPopupMode( QToolButton::MenuButtonPopup );
@@ -89,8 +91,10 @@ void QgsMapToolsDigitizingTechniqueManager::setupToolBars()
} );
mStreamDigitizingSettingsAction = new QgsStreamDigitizingSettingsAction( QgisApp::instance() );
+ mNurbsDigitizingSettingsAction = new QgsNurbsDigitizingSettingsAction( QgisApp::instance() );
digitizeMenu->addSeparator();
digitizeMenu->addAction( mStreamDigitizingSettingsAction );
+ digitizeMenu->addAction( mNurbsDigitizingSettingsAction );
mDigitizeModeToolButton->setMenu( digitizeMenu );
@@ -267,6 +271,9 @@ void QgsMapToolsDigitizingTechniqueManager::updateDigitizeModeButton( const Qgis
case Qgis::CaptureTechnique::Shape:
mDigitizeModeToolButton->setDefaultAction( QgisApp::instance()->mActionDigitizeShape );
break;
+ case Qgis::CaptureTechnique::NurbsCurve:
+ mDigitizeModeToolButton->setDefaultAction( QgisApp::instance()->mActionDigitizeWithNurbs );
+ break;
}
}
@@ -372,3 +379,71 @@ QgsStreamDigitizingSettingsAction::QgsStreamDigitizingSettingsAction( QWidget *p
}
QgsStreamDigitizingSettingsAction::~QgsStreamDigitizingSettingsAction() = default;
+
+//
+// QgsNurbsDigitizingSettingsAction
+//
+
+QgsNurbsDigitizingSettingsAction::QgsNurbsDigitizingSettingsAction( QWidget *parent )
+ : QWidgetAction( parent )
+{
+ QGridLayout *gLayout = new QGridLayout();
+ gLayout->setContentsMargins( 3, 2, 3, 2 );
+
+ // Mode ComboBox
+ mNurbsModeComboBox = new QComboBox();
+ mNurbsModeComboBox->addItem( tr( "Control Points" ), static_cast( Qgis::NurbsMode::ControlPoints ) );
+ mNurbsModeComboBox->addItem( tr( "Poly-Bézier" ), static_cast( Qgis::NurbsMode::PolyBezier ) );
+
+ // Set current index based on saved setting
+ const Qgis::NurbsMode currentMode = QgsSettingsRegistryCore::settingsDigitizingNurbsMode->value();
+ mNurbsModeComboBox->setCurrentIndex( mNurbsModeComboBox->findData( static_cast( currentMode ) ) );
+
+ QLabel *modeLabel = new QLabel( tr( "NURBS Mode" ) );
+ gLayout->addWidget( modeLabel, 0, 0 );
+ gLayout->addWidget( mNurbsModeComboBox, 0, 1 );
+
+ connect( mNurbsModeComboBox, qOverload( &QComboBox::currentIndexChanged ), this, &QgsNurbsDigitizingSettingsAction::updateDegreeEnabled );
+ connect( mNurbsModeComboBox, qOverload( &QComboBox::currentIndexChanged ), this, [this]( int index ) {
+ Q_UNUSED( index )
+ // Get the mode from the combo box data
+ Qgis::NurbsMode mode = static_cast( mNurbsModeComboBox->currentData().toInt() );
+ QgsSettingsRegistryCore::settingsDigitizingNurbsMode->setValue( mode );
+ } );
+
+ // Degree SpinBox
+ mNurbsDegreeSpinBox = new QgsSpinBox();
+ mNurbsDegreeSpinBox->setKeyboardTracking( false );
+ mNurbsDegreeSpinBox->setRange( 1, 5 );
+ mNurbsDegreeSpinBox->setWrapping( false );
+ mNurbsDegreeSpinBox->setSingleStep( 1 );
+ mNurbsDegreeSpinBox->setClearValue( 3 );
+ mNurbsDegreeSpinBox->setValue( QgsSettingsRegistryCore::settingsDigitizingNurbsDegree->value() );
+
+ mNurbsDegreeLabel = new QLabel( tr( "Degree" ) );
+ gLayout->addWidget( mNurbsDegreeLabel, 1, 0 );
+ gLayout->addWidget( mNurbsDegreeSpinBox, 1, 1 );
+ connect( mNurbsDegreeSpinBox, qOverload( &QgsSpinBox::valueChanged ), this, []( int value ) {
+ QgsSettingsRegistryCore::settingsDigitizingNurbsDegree->setValue( value );
+ } );
+
+ // Set initial enabled state based on current mode
+ const bool enableDegree = ( currentMode == Qgis::NurbsMode::ControlPoints );
+ mNurbsDegreeLabel->setEnabled( enableDegree );
+ mNurbsDegreeSpinBox->setEnabled( enableDegree );
+
+ QWidget *w = new QWidget( parent );
+ w->setLayout( gLayout );
+ setDefaultWidget( w );
+}
+
+QgsNurbsDigitizingSettingsAction::~QgsNurbsDigitizingSettingsAction() = default;
+
+void QgsNurbsDigitizingSettingsAction::updateDegreeEnabled( int modeIndex )
+{
+ Q_UNUSED( modeIndex )
+ const Qgis::NurbsMode mode = static_cast( mNurbsModeComboBox->currentData().toInt() );
+ const bool enableDegree = ( mode == Qgis::NurbsMode::ControlPoints );
+ mNurbsDegreeLabel->setEnabled( enableDegree );
+ mNurbsDegreeSpinBox->setEnabled( enableDegree );
+}
diff --git a/src/app/maptools/qgsmaptoolsdigitizingtechniquemanager.h b/src/app/maptools/qgsmaptoolsdigitizingtechniquemanager.h
index b7144c1b727f..69e4727be7d1 100644
--- a/src/app/maptools/qgsmaptoolsdigitizingtechniquemanager.h
+++ b/src/app/maptools/qgsmaptoolsdigitizingtechniquemanager.h
@@ -45,6 +45,26 @@ class APP_EXPORT QgsStreamDigitizingSettingsAction : public QWidgetAction
QgsSpinBox *mStreamToleranceSpinBox = nullptr;
};
+class QComboBox;
+class QLabel;
+
+class APP_EXPORT QgsNurbsDigitizingSettingsAction : public QWidgetAction
+{
+ Q_OBJECT
+
+ public:
+ QgsNurbsDigitizingSettingsAction( QWidget *parent = nullptr );
+ ~QgsNurbsDigitizingSettingsAction() override;
+
+ private slots:
+ void updateDegreeEnabled( int modeIndex );
+
+ private:
+ QComboBox *mNurbsModeComboBox = nullptr;
+ QgsSpinBox *mNurbsDegreeSpinBox = nullptr;
+ QLabel *mNurbsDegreeLabel = nullptr;
+};
+
class APP_EXPORT QgsMapToolsDigitizingTechniqueManager : public QObject
{
Q_OBJECT
@@ -83,6 +103,7 @@ class APP_EXPORT QgsMapToolsDigitizingTechniqueManager : public QObject
QToolButton *mDigitizeModeToolButton = nullptr;
QgsStreamDigitizingSettingsAction *mStreamDigitizingSettingsAction = nullptr;
+ QgsNurbsDigitizingSettingsAction *mNurbsDigitizingSettingsAction = nullptr;
};
#endif // QGSMAPTOOLSDIGITIZINGTECHNIQUEMANAGER_H
diff --git a/src/app/qgsmaptooladdpart.cpp b/src/app/qgsmaptooladdpart.cpp
index 6637f1164660..78be9475c00f 100644
--- a/src/app/qgsmaptooladdpart.cpp
+++ b/src/app/qgsmaptooladdpart.cpp
@@ -52,6 +52,7 @@ bool QgsMapToolAddPart::supportsTechnique( Qgis::CaptureTechnique technique ) co
case Qgis::CaptureTechnique::CircularString:
case Qgis::CaptureTechnique::Shape:
+ case Qgis::CaptureTechnique::NurbsCurve:
return mode() != QgsMapToolCapture::CapturePoint;
}
return false;
diff --git a/src/app/qgsmaptooladdring.cpp b/src/app/qgsmaptooladdring.cpp
index 84c26e8cdc98..f4e657661c5d 100644
--- a/src/app/qgsmaptooladdring.cpp
+++ b/src/app/qgsmaptooladdring.cpp
@@ -49,6 +49,7 @@ bool QgsMapToolAddRing::supportsTechnique( Qgis::CaptureTechnique technique ) co
case Qgis::CaptureTechnique::Streaming:
case Qgis::CaptureTechnique::CircularString:
case Qgis::CaptureTechnique::Shape:
+ case Qgis::CaptureTechnique::NurbsCurve:
return true;
}
return false;
diff --git a/src/app/qgsmaptoolfillring.cpp b/src/app/qgsmaptoolfillring.cpp
index abba2027cb2c..4d4aed1d63c9 100644
--- a/src/app/qgsmaptoolfillring.cpp
+++ b/src/app/qgsmaptoolfillring.cpp
@@ -46,6 +46,7 @@ bool QgsMapToolFillRing::supportsTechnique( Qgis::CaptureTechnique technique ) c
case Qgis::CaptureTechnique::Streaming:
case Qgis::CaptureTechnique::CircularString:
case Qgis::CaptureTechnique::Shape:
+ case Qgis::CaptureTechnique::NurbsCurve:
return true;
}
return false;
diff --git a/src/app/qgsmaptoolreshape.cpp b/src/app/qgsmaptoolreshape.cpp
index 4e806ba5992a..c9c7b7d7988f 100644
--- a/src/app/qgsmaptoolreshape.cpp
+++ b/src/app/qgsmaptoolreshape.cpp
@@ -85,6 +85,7 @@ bool QgsMapToolReshape::supportsTechnique( Qgis::CaptureTechnique technique ) co
case Qgis::CaptureTechnique::StraightSegments:
case Qgis::CaptureTechnique::CircularString:
case Qgis::CaptureTechnique::Streaming:
+ case Qgis::CaptureTechnique::NurbsCurve:
return true;
case Qgis::CaptureTechnique::Shape:
diff --git a/src/app/qgsmaptoolsplitfeatures.cpp b/src/app/qgsmaptoolsplitfeatures.cpp
index f943b4180fcd..51fc06b93c6a 100644
--- a/src/app/qgsmaptoolsplitfeatures.cpp
+++ b/src/app/qgsmaptoolsplitfeatures.cpp
@@ -39,6 +39,7 @@ bool QgsMapToolSplitFeatures::supportsTechnique( Qgis::CaptureTechnique techniqu
case Qgis::CaptureTechnique::StraightSegments:
case Qgis::CaptureTechnique::CircularString:
case Qgis::CaptureTechnique::Streaming:
+ case Qgis::CaptureTechnique::NurbsCurve:
return true;
case Qgis::CaptureTechnique::Shape:
diff --git a/src/app/qgsmaptoolsplitparts.cpp b/src/app/qgsmaptoolsplitparts.cpp
index bff3a3218029..807ed120e325 100644
--- a/src/app/qgsmaptoolsplitparts.cpp
+++ b/src/app/qgsmaptoolsplitparts.cpp
@@ -42,6 +42,7 @@ bool QgsMapToolSplitParts::supportsTechnique( Qgis::CaptureTechnique technique )
case Qgis::CaptureTechnique::CircularString:
case Qgis::CaptureTechnique::Shape:
+ case Qgis::CaptureTechnique::NurbsCurve:
return false;
}
return false;
diff --git a/src/app/vertextool/qgsvertexeditor.cpp b/src/app/vertextool/qgsvertexeditor.cpp
index b86db840a78e..a9d3a69d945e 100644
--- a/src/app/vertextool/qgsvertexeditor.cpp
+++ b/src/app/vertextool/qgsvertexeditor.cpp
@@ -27,6 +27,8 @@
#include "qgslockedfeature.h"
#include "qgsmapcanvas.h"
#include "qgsmessagelog.h"
+#include "qgsnurbscurve.h"
+#include "qgsnurbsutils.h"
#include "qgspanelwidgetstack.h"
#include "qgsproject.h"
#include "qgssettingsentryimpl.h"
@@ -67,6 +69,9 @@ void QgsVertexEditorModel::setFeature( QgsLockedFeature *lockedFeature )
beginResetModel();
mLockedFeature = lockedFeature;
+ mHasWeight = false;
+ mHasR = false; // Will be set to true only if geometry contains circular strings
+
if ( mLockedFeature && mLockedFeature->layer() )
{
const Qgis::WkbType layerWKBType = mLockedFeature->layer()->wkbType();
@@ -78,7 +83,19 @@ void QgsVertexEditorModel::setFeature( QgsLockedFeature *lockedFeature )
mMCol = mHasM ? ( 2 + ( mHasZ ? 1 : 0 ) ) : -1;
+ // Check if geometry contains NURBS curves (show Weight column)
+ // mHasR is already true by default (for circular strings)
+ if ( mLockedFeature->geometry() && mLockedFeature->geometry()->constGet() )
+ {
+ if ( QgsNurbsUtils::containsNurbsCurve( mLockedFeature->geometry()->constGet() ) )
+ {
+ mHasWeight = true;
+ }
+ }
+
+ // Calculate column indices after determining which columns are present
mRCol = mHasR ? ( 2 + ( mHasZ ? 1 : 0 ) + ( mHasM ? 1 : 0 ) ) : -1;
+ mWeightCol = mHasWeight ? ( 2 + ( mHasZ ? 1 : 0 ) + ( mHasM ? 1 : 0 ) + ( mHasR ? 1 : 0 ) ) : -1;
}
endResetModel();
@@ -98,7 +115,7 @@ int QgsVertexEditorModel::columnCount( const QModelIndex &parent ) const
if ( !mLockedFeature )
return 0;
else
- return 2 + ( mHasZ ? 1 : 0 ) + ( mHasM ? 1 : 0 ) + ( mHasR ? 1 : 0 );
+ return 2 + ( mHasZ ? 1 : 0 ) + ( mHasM ? 1 : 0 ) + ( mHasR ? 1 : 0 ) + ( mHasWeight ? 1 : 0 );
}
QVariant QgsVertexEditorModel::data( const QModelIndex &index, int role ) const
@@ -177,6 +194,13 @@ QVariant QgsVertexEditorModel::data( const QModelIndex &index, int role ) const
}
return QVariant();
}
+ else if ( index.column() == mWeightCol )
+ {
+ double w = getWeightForVertex( index.row() );
+ if ( w > 0 )
+ return w;
+ return QVariant();
+ }
else
{
return QVariant();
@@ -203,6 +227,8 @@ QVariant QgsVertexEditorModel::headerData( int section, Qt::Orientation orientat
return QVariant( tr( "m" ) );
else if ( section == mRCol )
return QVariant( tr( "r" ) );
+ else if ( section == mWeightCol )
+ return QVariant( tr( "w" ) );
else
return QVariant();
}
@@ -225,6 +251,8 @@ QVariant QgsVertexEditorModel::headerData( int section, Qt::Orientation orientat
return QVariant( tr( "M Value" ) );
else if ( section == mRCol )
return QVariant( tr( "Radius Value" ) );
+ else if ( section == mWeightCol )
+ return QVariant( tr( "NURBS Weight" ) );
else
return QVariant();
}
@@ -249,6 +277,18 @@ bool QgsVertexEditorModel::setData( const QModelIndex &index, const QVariant &va
// Get double value wrt current locale.
const double doubleValue { QgsDoubleValidator::toDouble( value.toString() ) };
+ // Handle weight column separately
+ if ( index.column() == mWeightCol )
+ {
+ if ( setWeightForVertex( index.row(), doubleValue ) )
+ {
+ mLockedFeature->layer()->triggerRepaint();
+ emit dataChanged( index, index );
+ return true;
+ }
+ return false;
+ }
+
double x = ( index.column() == 0 ? doubleValue : mLockedFeature->vertexMap().at( index.row() )->point().x() );
double y = ( index.column() == 1 ? doubleValue : mLockedFeature->vertexMap().at( index.row() )->point().y() );
@@ -321,6 +361,56 @@ bool QgsVertexEditorModel::calcR( int row, double &r, double &minRadius ) const
return true;
}
+double QgsVertexEditorModel::getWeightForVertex( int row ) const
+{
+ if ( !mLockedFeature || row < 0 || row >= mLockedFeature->vertexMap().count() )
+ return -1.0;
+
+ const QgsVertexEntry *entry = mLockedFeature->vertexMap().at( row );
+ const QgsVertexId vid = entry->vertexId();
+
+ const QgsAbstractGeometry *geom = mLockedFeature->geometry()->constGet();
+ int localIndex = 0;
+ const QgsNurbsCurve *nurbs = QgsNurbsUtils::findNurbsCurveForVertex( geom, vid, localIndex );
+
+ if ( nurbs )
+ return nurbs->weight( localIndex );
+
+ return -1.0;
+}
+
+bool QgsVertexEditorModel::setWeightForVertex( int row, double weight )
+{
+ if ( !mLockedFeature || !mLockedFeature->layer() || row < 0 || row >= mLockedFeature->vertexMap().count() )
+ return false;
+
+ if ( weight <= 0.0 )
+ return false;
+
+ const QgsVertexEntry *entry = mLockedFeature->vertexMap().at( row );
+ const QgsVertexId vid = entry->vertexId();
+
+ QgsGeometry *geom = mLockedFeature->geometry();
+ int localIndex = 0;
+ QgsNurbsCurve *nurbs = QgsNurbsUtils::findMutableNurbsCurveForVertex( geom->get(), vid, localIndex );
+
+ if ( !nurbs )
+ return false;
+
+ mLockedFeature->layer()->beginEditCommand( QObject::tr( "Changed NURBS weight" ) );
+
+ if ( nurbs->setWeight( localIndex, weight ) )
+ {
+ // Update the feature in the layer
+ mLockedFeature->layer()->changeGeometry( mLockedFeature->featureId(), *geom );
+ mLockedFeature->layer()->endEditCommand();
+ return true;
+ }
+
+ mLockedFeature->layer()->destroyEditCommand();
+ return false;
+}
+
//
// QgsVertexEditorWidget
//
@@ -402,6 +492,7 @@ void QgsVertexEditorWidget::updateEditor( QgsLockedFeature *lockedFeature )
mTableView->setItemDelegateForColumn( 2, new CoordinateItemDelegate( crs, this ) );
mTableView->setItemDelegateForColumn( 3, new CoordinateItemDelegate( crs, this ) );
mTableView->setItemDelegateForColumn( 4, new CoordinateItemDelegate( crs, this ) );
+ mTableView->setItemDelegateForColumn( 5, new CoordinateItemDelegate( crs, this ) );
}
}
else
diff --git a/src/app/vertextool/qgsvertexeditor.h b/src/app/vertextool/qgsvertexeditor.h
index a114fccbc1c0..37620e10f945 100644
--- a/src/app/vertextool/qgsvertexeditor.h
+++ b/src/app/vertextool/qgsvertexeditor.h
@@ -83,15 +83,29 @@ class APP_EXPORT QgsVertexEditorModel : public QAbstractTableModel
bool mHasZ = false;
bool mHasM = false;
- bool mHasR = true; //always show for now - avoids scanning whole feature for curves TODO - avoid this
+ bool mHasR = true; //always show for now - avoids scanning whole feature for curves TODO - avoid this
+ bool mHasWeight = false; // true if geometry contains NURBS curves
int mZCol = -1;
int mMCol = -1;
int mRCol = -1;
+ int mWeightCol = -1; // weight column for NURBS control points
QFont mWidgetFont;
bool calcR( int row, double &r, double &minRadius ) const;
+
+ /**
+ * Returns the weight for the vertex at the specified row.
+ * Returns -1 if the vertex is not a NURBS control point.
+ */
+ double getWeightForVertex( int row ) const;
+
+ /**
+ * Sets the weight for the vertex at the specified row.
+ * Returns true if successful.
+ */
+ bool setWeightForVertex( int row, double weight );
};
class APP_EXPORT QgsVertexEditorWidget : public QgsPanelWidget
diff --git a/src/app/vertextool/qgsvertextool.cpp b/src/app/vertextool/qgsvertextool.cpp
index ae3889254979..42442bfe6657 100644
--- a/src/app/vertextool/qgsvertextool.cpp
+++ b/src/app/vertextool/qgsvertextool.cpp
@@ -17,8 +17,12 @@
#include "qgisapp.h"
#include "qgsadvanceddigitizingdockwidget.h"
#include "qgsavoidintersectionsoperation.h"
+#include "qgscompoundcurve.h"
+#include "qgscoordinatetransform.h"
#include "qgscurve.h"
+#include "qgscurvepolygon.h"
#include "qgsexpressioncontextutils.h"
+#include "qgsgeometrycollection.h"
#include "qgsgeometryutils.h"
#include "qgsgeometryvalidator.h"
#include "qgsguiutils.h"
@@ -31,6 +35,8 @@
#include "qgsmessagelog.h"
#include "qgsmulticurve.h"
#include "qgsmultipoint.h"
+#include "qgsnurbscurve.h"
+#include "qgsnurbsutils.h"
#include "qgspointlocator.h"
#include "qgsproject.h"
#include "qgsrubberband.h"
@@ -56,9 +62,96 @@ uint qHash( const Vertex &v )
}
//
-// geomutils - may get moved elsewhere
+// geomutils - local helper for flat vertex index lookup
//
+/**
+ * Try to find a NURBS curve in the geometry and return it along with the vertex offset
+ * Returns nullptr if vertex is not part of a NURBS curve
+ */
+static const QgsNurbsCurve *findNurbsCurveForVertex( const QgsAbstractGeometry *geom, int vertexIndex, int &localVertexIndex )
+{
+ if ( !geom )
+ return nullptr;
+
+ if ( const QgsNurbsCurve *nurbs = qgsgeometry_cast( geom ) )
+ {
+ if ( vertexIndex < nurbs->numPoints() )
+ {
+ localVertexIndex = vertexIndex;
+ return nurbs;
+ }
+ return nullptr;
+ }
+
+ if ( const QgsGeometryCollection *gc = qgsgeometry_cast( geom ) )
+ {
+ int offset = 0;
+ for ( int i = 0; i < gc->numGeometries(); ++i )
+ {
+ const QgsAbstractGeometry *part = gc->geometryN( i );
+ int partVertexCount = part->vertexCount();
+ if ( vertexIndex < offset + partVertexCount )
+ {
+ const QgsNurbsCurve *result = findNurbsCurveForVertex( part, vertexIndex - offset, localVertexIndex );
+ if ( result )
+ return result;
+ }
+ offset += partVertexCount;
+ }
+ }
+
+ if ( const QgsCurvePolygon *cp = qgsgeometry_cast( geom ) )
+ {
+ int offset = 0;
+ if ( const QgsCurve *ext = cp->exteriorRing() )
+ {
+ int extCount = ext->vertexCount();
+ if ( vertexIndex < offset + extCount )
+ {
+ const QgsNurbsCurve *result = findNurbsCurveForVertex( ext, vertexIndex - offset, localVertexIndex );
+ if ( result )
+ return result;
+ }
+ offset += extCount;
+ }
+ for ( int i = 0; i < cp->numInteriorRings(); ++i )
+ {
+ const QgsCurve *ring = cp->interiorRing( i );
+ int ringCount = ring->vertexCount();
+ if ( vertexIndex < offset + ringCount )
+ {
+ const QgsNurbsCurve *result = findNurbsCurveForVertex( ring, vertexIndex - offset, localVertexIndex );
+ if ( result )
+ return result;
+ }
+ offset += ringCount;
+ }
+ }
+
+ if ( const QgsCompoundCurve *cc = qgsgeometry_cast( geom ) )
+ {
+ int offset = 0;
+ for ( int i = 0; i < cc->nCurves(); ++i )
+ {
+ const QgsCurve *curve = cc->curveAt( i );
+ int curveCount = curve->vertexCount();
+ // For compound curves, we need to subtract 1 for shared vertices (except first curve)
+ if ( i > 0 )
+ offset--; // Account for shared vertex with previous curve
+ if ( vertexIndex < offset + curveCount )
+ {
+ const QgsNurbsCurve *result = findNurbsCurveForVertex( curve, vertexIndex - offset, localVertexIndex );
+ if ( result )
+ return result;
+ }
+ offset += curveCount;
+ }
+ }
+
+ return nullptr;
+}
+
//! Find out whether vertex at the given index is an endpoint (assuming linear geometry)
static bool isEndpointAtVertexIndex( const QgsGeometry &geom, int vertexIndex )
@@ -292,6 +385,11 @@ QgsVertexTool::QgsVertexTool( QgsMapCanvas *canvas, QgsAdvancedDigitizingDockWid
mEndpointMarker->setIconSize( QgsGuiUtils::scaleIconSize( 10 ) );
mEndpointMarker->setPenWidth( QgsGuiUtils::scaleIconSize( 3 ) );
mEndpointMarker->setVisible( false );
+
+ // Control polygon for NURBS curves
+ mNurbsControlPolygonBand = std::make_unique( canvas, Qgis::GeometryType::Line );
+ applyControlPolygonStyle( mNurbsControlPolygonBand.get() );
+ mNurbsControlPolygonBand->setVisible( false );
}
QgsVertexTool::~QgsVertexTool()
@@ -302,6 +400,7 @@ QgsVertexTool::~QgsVertexTool()
delete mVertexBand;
delete mEdgeBand;
delete mEndpointMarker;
+ clearBezierVisuals();
}
void QgsVertexTool::activate()
@@ -411,6 +510,61 @@ void QgsVertexTool::addDragCircularBand( QgsVectorLayer *layer, QgsPointXY v0, Q
mDragCircularBands << b;
}
+void QgsVertexTool::addDragNurbsBand( QgsVectorLayer *layer, const QgsNurbsCurve *nurbs, const QSet &movingCtrlPointIndices, const QgsPointXY &mapPoint )
+{
+ if ( !nurbs || nurbs->controlPoints().isEmpty() )
+ return;
+
+ // Convert control points to map coordinates
+ QVector mapCtrlPts;
+ mapCtrlPts.reserve( nurbs->controlPoints().size() );
+
+ QgsCoordinateTransform ct;
+ if ( layer )
+ ct = QgsCoordinateTransform( layer->crs(), mCanvas->mapSettings().destinationCrs(), QgsProject::instance() );
+
+ for ( const QgsPoint &pt : nurbs->controlPoints() )
+ {
+ QgsPointXY mapPt( pt );
+ if ( ct.isValid() )
+ {
+ try
+ {
+ mapPt = ct.transform( mapPt );
+ }
+ catch ( QgsCsException & )
+ {
+ // keep original coordinates
+ }
+ }
+ mapCtrlPts.append( mapPt );
+ }
+
+ NurbsBand b;
+ b.curveBand = createRubberBand( Qgis::GeometryType::Line, true );
+ b.controlBand = createRubberBand( Qgis::GeometryType::Line, true );
+ applyControlPolygonStyle( b.controlBand );
+
+ b.controlPoints = mapCtrlPts;
+ b.degree = nurbs->degree();
+ b.knots = nurbs->knots();
+ b.weights = nurbs->weights();
+
+ // Set up moving indices and offsets
+ for ( int idx : movingCtrlPointIndices )
+ {
+ if ( idx >= 0 && idx < mapCtrlPts.size() )
+ {
+ b.movingIndices.append( idx );
+ b.offsets.append( mapCtrlPts[idx] - mapPoint );
+ }
+ }
+
+ b.updateRubberBand( mapPoint );
+
+ mDragNurbsBands << b;
+}
+
void QgsVertexTool::clearDragBands()
{
qDeleteAll( mDragPointMarkers );
@@ -424,6 +578,13 @@ void QgsVertexTool::clearDragBands()
for ( const CircularBand &b : std::as_const( mDragCircularBands ) )
delete b.band;
mDragCircularBands.clear();
+
+ for ( const NurbsBand &b : std::as_const( mDragNurbsBands ) )
+ {
+ delete b.curveBand;
+ delete b.controlBand;
+ }
+ mDragNurbsBands.clear();
}
void QgsVertexTool::cadCanvasPressEvent( QgsMapMouseEvent *e )
@@ -647,6 +808,24 @@ void QgsVertexTool::cadCanvasReleaseEvent( QgsMapMouseEvent *e )
}
else if ( e->button() == Qt::LeftButton && e->modifiers() & Qt::AltModifier )
{
+ // Check for Alt+click on poly-Bézier anchor to extend handles symmetrically
+ QgsPointLocator::Match m = snapToEditableLayer( e );
+ if ( m.isValid() && m.hasVertex() )
+ {
+ QgsGeometry geom = cachedGeometry( m.layer(), m.featureId() );
+ int localIdx = 0;
+ const QgsNurbsCurve *nurbs = findNurbsCurveForVertex( geom.constGet(), m.vertexIndex(), localIdx );
+ if ( nurbs && nurbs->isPolyBezier() && ( localIdx % 3 == 0 ) )
+ {
+ // This is an anchor on a poly-Bézier - start symmetric handle extension mode
+ mAltDragPolyBezierAnchor = true;
+ mAltDragAnchorIndex = localIdx;
+ emit messageEmitted( tr( "Poly-Bézier: drag to extend handles symmetrically" ) );
+ startDragging( e );
+ return;
+ }
+ }
+ // Not on a poly-Bézier anchor - use normal polygon selection
mSelectionMethod = SelectionPolygon;
initSelectionRubberBand();
mSelectionRubberBand->addPoint( toMapCoordinates( e->pos() ) );
@@ -763,6 +942,58 @@ void QgsVertexTool::moveDragBands( const QgsPointXY &mapPoint )
b.updateRubberBand( mapPoint );
}
+ for ( int i = 0; i < mDragNurbsBands.count(); ++i )
+ {
+ NurbsBand &b = mDragNurbsBands[i];
+ if ( mAltDragPolyBezierAnchor )
+ {
+ // Symmetric handle extension mode: anchor stays fixed, handles extend symmetrically
+ const int anchorIndex = mAltDragAnchorIndex;
+ if ( anchorIndex >= 0 && anchorIndex < b.controlPoints.size() )
+ {
+ QgsPointXY anchorPt = b.controlPoints[anchorIndex];
+
+ // Calculate vector from anchor to mouse
+ double dx = mapPoint.x() - anchorPt.x();
+ double dy = mapPoint.y() - anchorPt.y();
+
+ // Build updated control points
+ QVector updatedCtrlPts;
+ updatedCtrlPts.reserve( b.controlPoints.size() );
+
+ for ( int j = 0; j < b.controlPoints.size(); ++j )
+ {
+ if ( j == anchorIndex )
+ {
+ // Anchor stays fixed
+ updatedCtrlPts.append( QgsPoint( anchorPt ) );
+ }
+ else if ( j == anchorIndex + 1 && anchorIndex + 1 < b.controlPoints.size() )
+ {
+ // Handle after anchor - follows mouse direction
+ updatedCtrlPts.append( QgsPoint( anchorPt.x() + dx, anchorPt.y() + dy ) );
+ }
+ else if ( j == anchorIndex - 1 && anchorIndex > 0 )
+ {
+ // Handle before anchor - opposite direction (symmetric)
+ updatedCtrlPts.append( QgsPoint( anchorPt.x() - dx, anchorPt.y() - dy ) );
+ }
+ else
+ {
+ // Other control points stay static
+ updatedCtrlPts.append( QgsPoint( b.controlPoints[j] ) );
+ }
+ }
+
+ b.updateRubberBandFromPoints( updatedCtrlPts );
+ }
+ }
+ else
+ {
+ b.updateRubberBand( mapPoint );
+ }
+ }
+
// in case of moving of standalone point geometry
for ( int i = 0; i < mDragPointMarkers.count(); ++i )
{
@@ -1200,7 +1431,10 @@ void QgsVertexTool::mouseMoveNotDragging( QgsMapMouseEvent *e )
// if we are at an endpoint, let's show also the endpoint indicator
// so user can possibly add a new vertex at the end
- if ( isMatchAtEndpoint( m ) )
+ // but not for NURBS curves (endpoint addition not supported)
+ const QgsGeometry geom = cachedGeometry( m.layer(), m.featureId() );
+ const bool isNurbs = QgsNurbsUtils::containsNurbsCurve( geom.constGet() );
+ if ( isMatchAtEndpoint( m ) && !isNurbs )
{
mMouseAtEndpoint = std::make_unique< Vertex >( m.layer(), m.featureId(), m.vertexIndex() );
mEndpointMarkerCenter = std::make_unique< QgsPointXY >( positionForEndpointMarker( m ) );
@@ -1310,6 +1544,16 @@ void QgsVertexTool::updateVertexBand( const QgsPointLocator::Match &m )
}
}
+void QgsVertexTool::clearBezierVisuals()
+{
+ qDeleteAll( mBezierTangentBands );
+ mBezierTangentBands.clear();
+ qDeleteAll( mBezierAnchorMarkers );
+ mBezierAnchorMarkers.clear();
+ qDeleteAll( mBezierHandleMarkers );
+ mBezierHandleMarkers.clear();
+}
+
void QgsVertexTool::updateFeatureBand( const QgsPointLocator::Match &m )
{
// highlight feature
@@ -1318,9 +1562,135 @@ void QgsVertexTool::updateFeatureBand( const QgsPointLocator::Match &m )
if ( mFeatureBandLayer == m.layer() && mFeatureBandFid == m.featureId() )
return; // skip regeneration of rubber band if not needed
+ // Clear previous Bézier visuals
+ clearBezierVisuals();
+
QgsGeometry geom = cachedGeometry( m.layer(), m.featureId() );
- mFeatureBandMarkers->setToGeometry( geometryToMultiPoint( geom ), m.layer() );
- mFeatureBandMarkers->setVisible( true );
+
+ // Check if this is a NURBS curve and if it's a Poly-Bézier
+ const QgsNurbsCurve *nurbs = QgsNurbsUtils::extractNurbsCurve( geom.constGet() );
+ QVector ctrlPts = nurbs ? nurbs->controlPoints() : QVector();
+
+ if ( nurbs && !ctrlPts.isEmpty() )
+ {
+ QgsCoordinateTransform ct( m.layer()->crs(), mCanvas->mapSettings().destinationCrs(), QgsProject::instance() );
+
+ // Convert control points to map coordinates
+ QVector mapCtrlPts;
+ mapCtrlPts.reserve( ctrlPts.size() );
+ for ( const QgsPoint &pt : std::as_const( ctrlPts ) )
+ {
+ QgsPointXY mapPt( pt );
+ try
+ {
+ mapPt = ct.transform( mapPt );
+ }
+ catch ( QgsCsException & )
+ {
+ // keep original coordinates
+ }
+ mapCtrlPts.append( mapPt );
+ }
+
+ if ( nurbs->isPolyBezier() )
+ {
+ // Poly-Bézier mode: show anchors (squares) and handles (circles) separately
+ // Control points layout: [anchor0, handle_right0, handle_left1, anchor1, handle_right1, handle_left2, anchor2, ...]
+ // Anchors at indices: 0, 3, 6, 9, ... (i * 3)
+ // Handle rights at indices: 1, 4, 7, ... (i * 3 + 1)
+ // Handle lefts at indices: 2, 5, 8, ... (i * 3 + 2)
+
+ for ( int i = 0; i < ctrlPts.size(); ++i )
+ {
+ int localIdx = i % 3;
+
+ if ( localIdx == 0 )
+ {
+ // Anchor
+ QgsVertexMarker *marker = new QgsVertexMarker( mCanvas );
+ marker->setIconType( QgsVertexMarker::ICON_BOX );
+ const QColor snapColor = QgsSettingsRegistryCore::settingsDigitizingSnapColor->value();
+ marker->setColor( snapColor );
+ QColor fillColor = snapColor;
+ fillColor.setAlpha( 100 );
+ marker->setFillColor( fillColor );
+ marker->setIconSize( QgsGuiUtils::scaleIconSize( 10 ) );
+ marker->setPenWidth( QgsGuiUtils::scaleIconSize( 2 ) );
+ marker->setCenter( mapCtrlPts[i] );
+ marker->setVisible( true );
+ mBezierAnchorMarkers << marker;
+ }
+ else
+ {
+ // Handle
+ QgsVertexMarker *marker = new QgsVertexMarker( mCanvas );
+ marker->setIconType( QgsVertexMarker::ICON_CIRCLE );
+ QColor lineColor = QgsSettingsRegistryCore::settingsDigitizingLineColor->value();
+ int h, s, v, a;
+ lineColor.getHsv( &h, &s, &v, &a );
+ QColor handleColor = QColor::fromHsv( ( h + 120 ) % 360, s, v, a );
+ marker->setColor( handleColor );
+ QColor fillColor = handleColor;
+ fillColor.setAlpha( 100 );
+ marker->setFillColor( fillColor );
+ marker->setIconSize( QgsGuiUtils::scaleIconSize( 8 ) );
+ marker->setPenWidth( QgsGuiUtils::scaleIconSize( 2 ) );
+ marker->setCenter( mapCtrlPts[i] );
+ marker->setVisible( true );
+ mBezierHandleMarkers << marker;
+
+ // Create tangent line from anchor to handle
+ int anchorIndex = -1;
+ if ( localIdx == 1 )
+ {
+ // Handle right - connects to anchor at i-1
+ anchorIndex = i - 1;
+ }
+ else if ( localIdx == 2 )
+ {
+ // Handle left - connects to anchor at i+1
+ anchorIndex = i + 1;
+ }
+
+ if ( anchorIndex >= 0 && anchorIndex < mapCtrlPts.size() )
+ {
+ QgsRubberBand *tangentBand = new QgsRubberBand( mCanvas, Qgis::GeometryType::Line );
+ applyControlPolygonStyle( tangentBand );
+ tangentBand->addPoint( mapCtrlPts[anchorIndex] );
+ tangentBand->addPoint( mapCtrlPts[i] );
+ tangentBand->setVisible( true );
+ mBezierTangentBands << tangentBand;
+ }
+ }
+ }
+
+ // Hide the standard control polygon band and feature markers for Poly-Bézier
+ mNurbsControlPolygonBand->reset( Qgis::GeometryType::Line );
+ mNurbsControlPolygonBand->setVisible( false );
+ mFeatureBandMarkers->setVisible( false );
+ }
+ else
+ {
+ // CAD/Control Points mode: show simple control polygon
+ mNurbsControlPolygonBand->reset( Qgis::GeometryType::Line );
+ for ( const QgsPointXY &pt : std::as_const( mapCtrlPts ) )
+ {
+ mNurbsControlPolygonBand->addPoint( pt );
+ }
+ mNurbsControlPolygonBand->setVisible( true );
+ mFeatureBandMarkers->setToGeometry( geometryToMultiPoint( geom ), m.layer() );
+ mFeatureBandMarkers->setVisible( true );
+ }
+ }
+ else
+ {
+ // Not a NURBS curve
+ mNurbsControlPolygonBand->reset( Qgis::GeometryType::Line );
+ mNurbsControlPolygonBand->setVisible( false );
+ mFeatureBandMarkers->setToGeometry( geometryToMultiPoint( geom ), m.layer() );
+ mFeatureBandMarkers->setVisible( true );
+ }
+
if ( QgsWkbTypes::isCurvedType( geom.wkbType() ) )
geom = QgsGeometry( geom.constGet()->segmentize() );
mFeatureBand->setToGeometry( geom, m.layer() );
@@ -1332,6 +1702,9 @@ void QgsVertexTool::updateFeatureBand( const QgsPointLocator::Match &m )
{
mFeatureBand->setVisible( false );
mFeatureBandMarkers->setVisible( false );
+ mNurbsControlPolygonBand->reset( Qgis::GeometryType::Line );
+ mNurbsControlPolygonBand->setVisible( false );
+ clearBezierVisuals();
mFeatureBandLayer = nullptr;
mFeatureBandFid = QgsFeatureId();
}
@@ -1794,12 +2167,48 @@ void QgsVertexTool::buildDragBandsForVertices( const QSet &movingVertice
// i.e. every circular band is defined by its middle circular vertex
QSet verticesInCircularBands;
+ // set of NURBS curves already processed (identified by layer + feature id + pointer)
+ QSet, const QgsNurbsCurve *>> processedNurbsCurves;
+
for ( const Vertex &v : std::as_const( movingVertices ) )
{
int v0idx, v1idx;
QgsGeometry geom = cachedGeometry( v.layer, v.fid );
QgsPointXY pt = geom.vertexAt( v.vertexId );
+ // Check if this vertex belongs to a NURBS curve
+ int localIdx = 0;
+ const QgsNurbsCurve *nurbs = findNurbsCurveForVertex( geom.constGet(), v.vertexId, localIdx );
+ if ( nurbs )
+ {
+ auto nurbsKey = qMakePair( qMakePair( v.layer, v.fid ), nurbs );
+ if ( !processedNurbsCurves.contains( nurbsKey ) )
+ {
+ // Build the set of moving control point indices for this NURBS
+ QSet movingCtrlPointIndices;
+ movingCtrlPointIndices.insert( localIdx );
+
+ // Also check other moving vertices that might be on the same NURBS
+ for ( const Vertex &otherV : movingVertices )
+ {
+ if ( otherV.layer == v.layer && otherV.fid == v.fid && otherV != v )
+ {
+ int otherLocalIdx = 0;
+ const QgsNurbsCurve *otherNurbs = findNurbsCurveForVertex( geom.constGet(), otherV.vertexId, otherLocalIdx );
+ if ( otherNurbs == nurbs )
+ {
+ movingCtrlPointIndices.insert( otherLocalIdx );
+ }
+ }
+ }
+
+ addDragNurbsBand( v.layer, nurbs, movingCtrlPointIndices, dragVertexMapPoint );
+ processedNurbsCurves.insert( nurbsKey );
+ }
+ // NURBS vertices don't need straight bands - the NURBS band handles visualization
+ continue;
+ }
+
geom.adjacentVertices( v.vertexId, v0idx, v1idx );
if ( v0idx != -1 && v1idx != -1 && isCircularVertex( geom, v.vertexId ) )
@@ -2038,6 +2447,8 @@ void QgsVertexTool::stopDragging()
mDraggingVertex.reset();
mDraggingVertexType = NotDragging;
mDraggingEdge = false;
+ mAltDragPolyBezierAnchor = false;
+ mAltDragAnchorIndex = -1;
clearDragBands();
setHighlightedVerticesVisible( true ); // highlight can be shown again
@@ -2053,6 +2464,7 @@ QgsPoint QgsVertexTool::matchToLayerPoint( const QgsVectorLayer *destLayer, cons
{
case QgsPointLocator::Vertex:
case QgsPointLocator::LineEndpoint:
+ case QgsPointLocator::ControlPoint:
case QgsPointLocator::All:
{
// use point coordinates of the layer
@@ -2122,6 +2534,11 @@ void QgsVertexTool::moveVertex( const QgsPointXY &mapPoint, const QgsPointLocato
bool addingVertex = mDraggingVertexType == AddingVertex || mDraggingVertexType == AddingEndpoint;
bool addingAtEndpoint = mDraggingVertexType == AddingEndpoint;
QgsGeometry geom = cachedGeometryForVertex( *mDraggingVertex );
+
+ // Store Alt+drag poly-Bézier state before stopDragging resets it
+ const bool wasAltDragPolyBezier = mAltDragPolyBezierAnchor;
+ const int altDragAnchorIdx = mAltDragAnchorIndex;
+
stopDragging();
QgsPoint layerPoint = matchToLayerPoint( dragLayer, mapPoint, mapPointMatch );
@@ -2210,6 +2627,65 @@ void QgsVertexTool::moveVertex( const QgsPointXY &mapPoint, const QgsPointLocato
return;
}
}
+ else if ( wasAltDragPolyBezier )
+ {
+ // Alt+drag on poly-Bézier anchor: move handles symmetrically, anchor stays fixed
+ int localIdx = 0;
+ QgsNurbsCurve *nurbsCurve = QgsNurbsUtils::findMutableNurbsCurveForVertex( geomTmp.get(), vid, localIdx );
+ if ( nurbsCurve && nurbsCurve->isPolyBezier() && altDragAnchorIdx >= 0 )
+ {
+ const QVector &ctrlPts = nurbsCurve->controlPoints();
+
+ // Get anchor position (stays fixed)
+ const QgsPoint &anchorPt = ctrlPts.at( altDragAnchorIdx );
+
+ // Calculate vector from anchor to mouse position in layer coordinates
+ const double dx = layerPoint.x() - anchorPt.x();
+ const double dy = layerPoint.y() - anchorPt.y();
+
+ // Calculate base vertex offset: vid.vertex points to the clicked vertex,
+ // localIdx is its position within the NURBS, so the NURBS starts at vid.vertex - localIdx
+ const int nurbsStartVertex = vid.vertex - localIdx;
+
+ // Move handle after anchor (altDragAnchorIdx + 1) - follows mouse direction
+ const int handleAfterIdx = altDragAnchorIdx + 1;
+ if ( handleAfterIdx < ctrlPts.size() )
+ {
+ const QgsPoint &originalHandle = ctrlPts.at( handleAfterIdx );
+ QgsPoint handleAfter( anchorPt.x() + dx, anchorPt.y() + dy );
+ // Preserve original Z/M values from the handle itself
+ if ( originalHandle.is3D() )
+ handleAfter.addZValue( originalHandle.z() );
+ if ( originalHandle.isMeasure() )
+ handleAfter.addMValue( originalHandle.m() );
+
+ QgsVertexId handleAfterId( vid.part, vid.ring, nurbsStartVertex + handleAfterIdx );
+ if ( !geomTmp->moveVertex( handleAfterId, handleAfter ) )
+ {
+ QgsDebugError( QStringLiteral( "move handle after failed!" ) );
+ }
+ }
+
+ // Move handle before anchor (altDragAnchorIdx - 1) - opposite direction (symmetric)
+ const int handleBeforeIdx = altDragAnchorIdx - 1;
+ if ( handleBeforeIdx >= 0 )
+ {
+ const QgsPoint &originalHandle = ctrlPts.at( handleBeforeIdx );
+ QgsPoint handleBefore( anchorPt.x() - dx, anchorPt.y() - dy );
+ // Preserve original Z/M values from the handle itself
+ if ( originalHandle.is3D() )
+ handleBefore.addZValue( originalHandle.z() );
+ if ( originalHandle.isMeasure() )
+ handleBefore.addMValue( originalHandle.m() );
+
+ QgsVertexId handleBeforeId( vid.part, vid.ring, nurbsStartVertex + handleBeforeIdx );
+ if ( !geomTmp->moveVertex( handleBeforeId, handleBefore ) )
+ {
+ QgsDebugError( QStringLiteral( "move handle before failed!" ) );
+ }
+ }
+ }
+ }
else
{
if ( !geomTmp->moveVertex( vid, layerPoint ) )
@@ -2926,6 +3402,55 @@ void QgsVertexTool::CircularBand::updateRubberBand( const QgsPointXY &mapPoint )
}
+void QgsVertexTool::NurbsBand::updateRubberBandFromPoints( const QVector &updatedCtrlPts )
+{
+ // Update control polygon rubberband
+ controlBand->reset( Qgis::GeometryType::Line );
+ for ( const QgsPoint &pt : std::as_const( updatedCtrlPts ) )
+ controlBand->addPoint( QgsPointXY( pt ) );
+
+ // Create temporary NURBS curve and evaluate it
+ if ( updatedCtrlPts.size() >= degree + 1 )
+ {
+ QgsNurbsCurve tempCurve( updatedCtrlPts, degree, knots, weights );
+ std::unique_ptr line( tempCurve.curveToLine() );
+
+ curveBand->reset( Qgis::GeometryType::Line );
+ if ( line )
+ {
+ for ( int i = 0; i < line->numPoints(); ++i )
+ curveBand->addPoint( line->pointN( i ) );
+ }
+ }
+}
+
+
+void QgsVertexTool::NurbsBand::updateRubberBand( const QgsPointXY &mapPoint )
+{
+ // Build updated control points
+ QVector updatedCtrlPts;
+ updatedCtrlPts.reserve( controlPoints.size() );
+
+ for ( int i = 0; i < controlPoints.size(); ++i )
+ {
+ int movingIdx = movingIndices.indexOf( i );
+ if ( movingIdx >= 0 )
+ {
+ // This control point is moving
+ QgsPointXY newPt = mapPoint + offsets[movingIdx];
+ updatedCtrlPts.append( QgsPoint( newPt ) );
+ }
+ else
+ {
+ // This control point is static
+ updatedCtrlPts.append( QgsPoint( controlPoints[i] ) );
+ }
+ }
+
+ updateRubberBandFromPoints( updatedCtrlPts );
+}
+
+
void QgsVertexTool::validationErrorFound( const QgsGeometry::Error &e )
{
QgsGeometryValidator *validator = qobject_cast( sender() );
diff --git a/src/app/vertextool/qgsvertextool.h b/src/app/vertextool/qgsvertextool.h
index 8a66b97d1eaf..efb6763647f9 100644
--- a/src/app/vertextool/qgsvertextool.h
+++ b/src/app/vertextool/qgsvertextool.h
@@ -29,6 +29,7 @@
class QRubberBand;
class QgsGeometryValidator;
+class QgsNurbsCurve;
class QgsVertexEditor;
class QgsLockedFeature;
class QgsSnapIndicator;
@@ -134,6 +135,8 @@ class APP_EXPORT QgsVertexTool : public QgsMapToolAdvancedDigitizing
void addDragCircularBand( QgsVectorLayer *layer, QgsPointXY v0, QgsPointXY v1, QgsPointXY v2, bool moving0, bool moving1, bool moving2, const QgsPointXY &mapPoint );
+ void addDragNurbsBand( QgsVectorLayer *layer, const QgsNurbsCurve *nurbs, const QSet &movingCtrlPointIndices, const QgsPointXY &mapPoint );
+
void moveDragBands( const QgsPointXY &mapPoint );
void clearDragBands();
@@ -297,6 +300,9 @@ class APP_EXPORT QgsVertexTool : public QgsMapToolAdvancedDigitizing
void updateFeatureBand( const QgsPointLocator::Match &m );
+ //! Clears Poly-Bézier visual elements (tangent lines, anchor and handle markers)
+ void clearBezierVisuals();
+
//! Updates vertex band based on the current match
void updateVertexBand( const QgsPointLocator::Match &m );
@@ -381,10 +387,42 @@ class APP_EXPORT QgsVertexTool : public QgsMapToolAdvancedDigitizing
void updateRubberBand( const QgsPointXY &mapPoint );
};
+ //! structure to keep information about a rubber band used for dragging of a NURBS curve
+ struct NurbsBand
+ {
+ QgsRubberBand *curveBand = nullptr; //!< Rubber band for the evaluated NURBS curve
+ QgsRubberBand *controlBand = nullptr; //!< Rubber band for the control polygon
+ QVector controlPoints; //!< Original control points (in map coordinates)
+ QVector movingIndices; //!< Indices of control points that are moving
+ QVector offsets; //!< Offsets for moving control points from mouse cursor
+ int degree = 3; //!< Degree of the NURBS curve
+ QVector knots; //!< Knot vector
+ QVector weights; //!< Weights
+
+ //! Update geometry of the rubber bands on the current mouse cursor position (in map units)
+ void updateRubberBand( const QgsPointXY &mapPoint );
+
+ //! Update geometry of the rubber bands from pre-calculated control points
+ void updateRubberBandFromPoints( const QVector &updatedCtrlPts );
+ };
+
//! list of active straight line rubber bands
QList mDragStraightBands;
//! list of active rubber bands for circular segments
QList mDragCircularBands;
+ //! list of active rubber bands for NURBS curves
+ QList mDragNurbsBands;
+
+ //! rubber band for displaying NURBS control polygon in edit mode
+ std::unique_ptr mNurbsControlPolygonBand;
+
+ //! rubber bands for displaying Poly-Bézier tangent lines (anchor to handle)
+ QList mBezierTangentBands;
+ //! markers for Poly-Bézier anchors (squares)
+ QList mBezierAnchorMarkers;
+ //! markers for Poly-Bézier handles (circles)
+ QList mBezierHandleMarkers;
+
//! instance of Vertex that is being currently moved or nothing
std::unique_ptr mDraggingVertex;
//! whether moving a vertex or adding one
@@ -392,6 +430,11 @@ class APP_EXPORT QgsVertexTool : public QgsMapToolAdvancedDigitizing
//! whether we are currently dragging an edge
bool mDraggingEdge = false;
+ //! Whether Alt+drag on poly-Bézier anchor is active (symmetric handle extension)
+ bool mAltDragPolyBezierAnchor = false;
+ //! Index of the anchor being Alt+dragged in the NURBS control points (0, 3, 6, ...)
+ int mAltDragAnchorIndex = -1;
+
/**
* list of Vertex instances of further vertices that are dragged together with
* the main vertex (mDraggingVertex) - either topologically connected points
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index ae8f3ab26437..f5ae921520a0 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -937,6 +937,8 @@ set(QGIS_CORE_SRCS
geometry/qgsmultipoint.cpp
geometry/qgsmultipolygon.cpp
geometry/qgsmultisurface.cpp
+ geometry/qgsnurbscurve.cpp
+ geometry/qgsnurbsutils.cpp
geometry/qgsorientedbox3d.cpp
geometry/qgspoint.cpp
geometry/qgspolygon.cpp
@@ -1535,6 +1537,8 @@ set(QGIS_CORE_HDRS
geometry/qgsmultipoint.h
geometry/qgsmultipolygon.h
geometry/qgsmultisurface.h
+ geometry/qgsnurbscurve.h
+ geometry/qgsnurbsutils.h
geometry/qgsorientedbox3d.h
geometry/qgspoint.h
geometry/qgspolygon.h
diff --git a/src/core/geometry/qgsabstractgeometry.h b/src/core/geometry/qgsabstractgeometry.h
index b2358494b851..0c238f02cc05 100644
--- a/src/core/geometry/qgsabstractgeometry.h
+++ b/src/core/geometry/qgsabstractgeometry.h
@@ -90,6 +90,8 @@ class CORE_EXPORT QgsAbstractGeometry
sipType = sipType_QgsCircularString;
else if ( qgsgeometry_cast( sipCpp ) != nullptr )
sipType = sipType_QgsCompoundCurve;
+ else if ( qgsgeometry_cast( sipCpp ) != nullptr )
+ sipType = sipType_QgsNurbsCurve;
else if ( qgsgeometry_cast( sipCpp ) != nullptr )
sipType = sipType_QgsTriangle;
else if ( qgsgeometry_cast( sipCpp ) != nullptr )
diff --git a/src/core/geometry/qgsgeometry.cpp b/src/core/geometry/qgsgeometry.cpp
index 6ab2ebaa6998..6deb12dcfa78 100644
--- a/src/core/geometry/qgsgeometry.cpp
+++ b/src/core/geometry/qgsgeometry.cpp
@@ -51,8 +51,8 @@ email : morb at ozemail dot com dot au
struct QgsGeometryPrivate
{
- QgsGeometryPrivate(): ref( 1 ) {}
- QgsGeometryPrivate( std::unique_ptr< QgsAbstractGeometry > geometry ): ref( 1 ), geometry( std::move( geometry ) ) {}
+ QgsGeometryPrivate() : ref( 1 ) {}
+ QgsGeometryPrivate( std::unique_ptr< QgsAbstractGeometry > geometry ) : ref( 1 ), geometry( std::move( geometry ) ) {}
QAtomicInt ref;
std::unique_ptr< QgsAbstractGeometry > geometry;
};
@@ -118,7 +118,7 @@ void QgsGeometry::reset( std::unique_ptr newGeometry )
{
if ( d->ref > 1 )
{
- ( void )d->ref.deref();
+ ( void ) d->ref.deref();
d = new QgsGeometryPrivate();
}
d->geometry = std::move( newGeometry );
@@ -151,7 +151,7 @@ bool QgsGeometry::isNull() const
}
typedef QCache< QString, QgsGeometry > WktCache;
-Q_GLOBAL_STATIC_WITH_ARGS( WktCache, sWktCache, ( 2000 ) ) // store up to 2000 geometries
+Q_GLOBAL_STATIC_WITH_ARGS( WktCache, sWktCache, ( 2000 ) ) // store up to 2000 geometries
Q_GLOBAL_STATIC( QMutex, sWktMutex )
QgsGeometry QgsGeometry::fromWkt( const QString &wkt )
@@ -249,7 +249,8 @@ QgsGeometry QgsGeometry::fromRect( const QgsRectangle &rect )
<< rect.yMinimum()
<< rect.yMaximum()
<< rect.yMaximum()
- << rect.yMinimum() );
+ << rect.yMinimum()
+ );
auto polygon = std::make_unique< QgsPolygon >();
polygon->setExteriorRing( ext.release() );
return QgsGeometry( std::move( polygon ) );
@@ -279,7 +280,8 @@ QgsGeometry QgsGeometry::fromBox3D( const QgsBox3D &box )
<< box.zMinimum()
<< box.zMinimum()
<< box.zMinimum()
- << box.zMinimum() );
+ << box.zMinimum()
+ );
auto polygon1 = std::make_unique< QgsPolygon >( ext1.release() );
polyhedralSurface->addPatch( polygon1.release() );
@@ -298,7 +300,8 @@ QgsGeometry QgsGeometry::fromBox3D( const QgsBox3D &box )
<< box.zMinimum()
<< box.zMaximum()
<< box.zMaximum()
- << box.zMinimum() );
+ << box.zMinimum()
+ );
auto polygon2 = std::make_unique< QgsPolygon >( ext2.release() );
polyhedralSurface->addPatch( polygon2.release() );
@@ -317,7 +320,8 @@ QgsGeometry QgsGeometry::fromBox3D( const QgsBox3D &box )
<< box.zMinimum()
<< box.zMaximum()
<< box.zMaximum()
- << box.zMinimum() );
+ << box.zMinimum()
+ );
auto polygon3 = std::make_unique< QgsPolygon >( ext3.release() );
polyhedralSurface->addPatch( polygon3.release() );
@@ -336,7 +340,8 @@ QgsGeometry QgsGeometry::fromBox3D( const QgsBox3D &box )
<< box.zMaximum()
<< box.zMaximum()
<< box.zMaximum()
- << box.zMaximum() );
+ << box.zMaximum()
+ );
auto polygon4 = std::make_unique< QgsPolygon >( ext4.release() );
polyhedralSurface->addPatch( polygon4.release() );
@@ -355,7 +360,8 @@ QgsGeometry QgsGeometry::fromBox3D( const QgsBox3D &box )
<< box.zMaximum()
<< box.zMinimum()
<< box.zMinimum()
- << box.zMaximum() );
+ << box.zMaximum()
+ );
auto polygon5 = std::make_unique< QgsPolygon >( ext5.release() );
polyhedralSurface->addPatch( polygon5.release() );
@@ -374,7 +380,8 @@ QgsGeometry QgsGeometry::fromBox3D( const QgsBox3D &box )
<< box.zMinimum()
<< box.zMinimum()
<< box.zMaximum()
- << box.zMaximum() );
+ << box.zMaximum()
+ );
auto polygon6 = std::make_unique< QgsPolygon >( ext6.release() );
polyhedralSurface->addPatch( polygon6.release() );
@@ -425,7 +432,7 @@ QgsGeometry QgsGeometry::createWedgeBufferFromAngles( const QgsPoint ¢er, do
const double DEG_TO_RAD = M_PI / 180.0;
const double RAD_TO_DEG = 180.0 / M_PI;
- const double angularWidth = endAngle - startAngle;
+ const double angularWidth = endAngle - startAngle;
const bool useShortestArc = QgsGeometryUtilsBase::normalizedAngle( angularWidth * DEG_TO_RAD ) * RAD_TO_DEG <= 180.0;
if ( std::abs( angularWidth ) >= 360.0 )
@@ -479,7 +486,7 @@ void QgsGeometry::fromWkb( unsigned char *wkb, int length )
{
QgsConstWkbPtr ptr( wkb, length );
reset( QgsGeometryFactory::geomFromWkb( ptr ) );
- delete [] wkb;
+ delete[] wkb;
}
void QgsGeometry::fromWkb( const QByteArray &wkb )
@@ -679,7 +686,6 @@ bool QgsGeometry::deleteVertex( int atVertex )
bool QgsGeometry::toggleCircularAtVertex( int atVertex )
{
-
if ( !d->geometry )
return false;
@@ -713,7 +719,7 @@ bool QgsGeometry::toggleCircularAtVertex( int atVertex )
return false;
bool success = false;
- QgsCompoundCurve *cpdCurve = qgsgeometry_cast( curve );
+ QgsCompoundCurve *cpdCurve = qgsgeometry_cast( curve );
if ( cpdCurve )
{
// If the geom is a already compound curve, we convert inplace, and we're done
@@ -832,7 +838,7 @@ bool QgsGeometry::addTopologicalPoint( const QgsPoint &point, double snappingTol
closestVertex( point, atVertex, beforeVertex, afterVertex, sqrDistVertexSnap );
if ( sqrDistVertexSnap < sqrSnappingTolerance )
- return false; // the vertex already exists - do not insert it
+ return false; // the vertex already exists - do not insert it
if ( !insertVertex( point, segmentAfterVertex ) )
{
@@ -851,7 +857,7 @@ QgsPoint QgsGeometry::vertexAt( int atVertex ) const
}
QgsVertexId vId;
- ( void )vertexIdFromVertexNr( atVertex, vId );
+ ( void ) vertexIdFromVertexNr( atVertex, vId );
if ( vId.vertex < 0 )
{
return QgsPoint();
@@ -911,11 +917,7 @@ double QgsGeometry::closestVertexWithContext( const QgsPointXY &point, int &atVe
return QgsGeometryUtils::sqrDistance2D( closestPoint, pt );
}
-double QgsGeometry::closestSegmentWithContext( const QgsPointXY &point,
- QgsPointXY &minDistPoint,
- int &nextVertexIndex,
- int *leftOrRightOfSegment,
- double epsilon ) const
+double QgsGeometry::closestSegmentWithContext( const QgsPointXY &point, QgsPointXY &minDistPoint, int &nextVertexIndex, int *leftOrRightOfSegment, double epsilon ) const
{
if ( !d->geometry )
{
@@ -925,7 +927,7 @@ double QgsGeometry::closestSegmentWithContext( const QgsPointXY &point,
QgsPoint segmentPt;
QgsVertexId vertexAfter;
- double sqrDist = d->geometry->closestSegment( QgsPoint( point ), segmentPt, vertexAfter, leftOrRightOfSegment, epsilon );
+ double sqrDist = d->geometry->closestSegment( QgsPoint( point ), segmentPt, vertexAfter, leftOrRightOfSegment, epsilon );
if ( sqrDist < 0 )
return -1;
@@ -1443,7 +1445,6 @@ QgsGeometry QgsGeometry::minimalEnclosingCircle( QgsPointXY ¢er, double &rad
QgsGeometry geom;
geom.set( circ.toPolygon( segments ) );
return geom;
-
}
QgsGeometry QgsGeometry::minimalEnclosingCircle( unsigned int segments ) const
@@ -1451,7 +1452,6 @@ QgsGeometry QgsGeometry::minimalEnclosingCircle( unsigned int segments ) const
QgsPointXY center;
double radius;
return minimalEnclosingCircle( center, radius, segments );
-
}
QgsGeometry QgsGeometry::orthogonalize( double tolerance, int maxIterations, double angleThreshold ) const
@@ -1714,7 +1714,6 @@ json QgsGeometry::asJsonObject( int precision ) const
return nullptr;
}
return d->geometry->asJsonObject( precision );
-
}
QVector QgsGeometry::coerceToType( const Qgis::WkbType type, double defaultZ, double defaultM, bool avoidDuplicates ) const
@@ -1742,16 +1741,63 @@ QVector QgsGeometry::coerceToType( const Qgis::WkbType type, double
newGeom = QgsGeometry( d->geometry.get()->segmentize() );
}
+ if ( QgsWkbTypes::isCurvedType( type ) && QgsWkbTypes::flatType( type ) != Qgis::WkbType::NurbsCurve )
+ {
+ // Check if geometry contains NurbsCurve that needs conversion
+ bool hasNurbs = false;
+ const Qgis::WkbType flatGeomType = QgsWkbTypes::flatType( newGeom.wkbType() );
+ if ( flatGeomType == Qgis::WkbType::NurbsCurve )
+ {
+ hasNurbs = true;
+ }
+ else if ( const QgsGeometryCollection *collection = qgsgeometry_cast< const QgsGeometryCollection * >( newGeom.constGet() ) )
+ {
+ for ( int i = 0; i < collection->numGeometries(); ++i )
+ {
+ if ( QgsWkbTypes::flatType( collection->geometryN( i )->wkbType() ) == Qgis::WkbType::NurbsCurve )
+ {
+ hasNurbs = true;
+ break;
+ }
+ }
+ }
+ else if ( const QgsCurvePolygon *cp = qgsgeometry_cast< const QgsCurvePolygon * >( newGeom.constGet() ) )
+ {
+ if ( cp->exteriorRing() && QgsWkbTypes::flatType( cp->exteriorRing()->wkbType() ) == Qgis::WkbType::NurbsCurve )
+ hasNurbs = true;
+ for ( int i = 0; !hasNurbs && i < cp->numInteriorRings(); ++i )
+ {
+ if ( QgsWkbTypes::flatType( cp->interiorRing( i )->wkbType() ) == Qgis::WkbType::NurbsCurve )
+ hasNurbs = true;
+ }
+ }
+ else if ( const QgsCompoundCurve *cc = qgsgeometry_cast< const QgsCompoundCurve * >( newGeom.constGet() ) )
+ {
+ for ( int i = 0; i < cc->nCurves(); ++i )
+ {
+ if ( QgsWkbTypes::flatType( cc->curveAt( i )->wkbType() ) == Qgis::WkbType::NurbsCurve )
+ {
+ hasNurbs = true;
+ break;
+ }
+ }
+ }
+
+ if ( hasNurbs )
+ {
+ // Segmentize to remove NURBS, then we'll convert back to curve type below
+ newGeom = QgsGeometry( newGeom.constGet()->segmentize() );
+ }
+ }
+
// polygon -> line
- if ( QgsWkbTypes::geometryType( type ) == Qgis::GeometryType::Line &&
- newGeom.type() == Qgis::GeometryType::Polygon )
+ if ( QgsWkbTypes::geometryType( type ) == Qgis::GeometryType::Line && newGeom.type() == Qgis::GeometryType::Polygon )
{
// boundary gives us a (multi)line string of exterior + interior rings
newGeom = QgsGeometry( newGeom.constGet()->boundary() );
}
// line -> polygon
- if ( QgsWkbTypes::geometryType( type ) == Qgis::GeometryType::Polygon &&
- newGeom.type() == Qgis::GeometryType::Line )
+ if ( QgsWkbTypes::geometryType( type ) == Qgis::GeometryType::Polygon && newGeom.type() == Qgis::GeometryType::Line )
{
std::unique_ptr< QgsGeometryCollection > gc( QgsGeometryFactory::createCollectionOfType( type ) );
const QgsGeometry source = newGeom;
@@ -1764,14 +1810,14 @@ QVector QgsGeometry::coerceToType( const Qgis::WkbType type, double
{
auto cp = std::make_unique< QgsCurvePolygon >();
cp->setExteriorRing( curve );
- ( void )exterior.release();
+ ( void ) exterior.release();
gc->addGeometry( cp.release() );
}
else
{
- auto p = std::make_unique< QgsPolygon >();
+ auto p = std::make_unique< QgsPolygon >();
p->setExteriorRing( qgsgeometry_cast< QgsLineString * >( curve ) );
- ( void )exterior.release();
+ ( void ) exterior.release();
gc->addGeometry( p.release() );
}
}
@@ -1780,9 +1826,7 @@ QVector QgsGeometry::coerceToType( const Qgis::WkbType type, double
}
// line/polygon -> points
- if ( QgsWkbTypes::geometryType( type ) == Qgis::GeometryType::Point &&
- ( newGeom.type() == Qgis::GeometryType::Line ||
- newGeom.type() == Qgis::GeometryType::Polygon ) )
+ if ( QgsWkbTypes::geometryType( type ) == Qgis::GeometryType::Point && ( newGeom.type() == Qgis::GeometryType::Line || newGeom.type() == Qgis::GeometryType::Polygon ) )
{
// lines/polygons to a point layer, extract all vertices
auto mp = std::make_unique< QgsMultiPoint >();
@@ -1799,8 +1843,7 @@ QVector QgsGeometry::coerceToType( const Qgis::WkbType type, double
}
//(Multi)Polygon to PolyhedralSurface
- if ( QgsWkbTypes::flatType( type ) == Qgis::WkbType::PolyhedralSurface &&
- QgsWkbTypes::flatType( QgsWkbTypes::singleType( newGeom.wkbType() ) ) == Qgis::WkbType::Polygon )
+ if ( QgsWkbTypes::flatType( type ) == Qgis::WkbType::PolyhedralSurface && QgsWkbTypes::flatType( QgsWkbTypes::singleType( newGeom.wkbType() ) ) == Qgis::WkbType::Polygon )
{
auto polySurface = std::make_unique< QgsPolyhedralSurface >();
const QgsGeometry source = newGeom;
@@ -1815,8 +1858,7 @@ QVector QgsGeometry::coerceToType( const Qgis::WkbType type, double
}
// Polygon -> Triangle
- if ( QgsWkbTypes::flatType( type ) == Qgis::WkbType::Triangle &&
- QgsWkbTypes::flatType( newGeom.wkbType() ) == Qgis::WkbType::Polygon )
+ if ( QgsWkbTypes::flatType( type ) == Qgis::WkbType::Triangle && QgsWkbTypes::flatType( newGeom.wkbType() ) == Qgis::WkbType::Polygon )
{
auto triangle = std::make_unique< QgsTriangle >();
const QgsGeometry source = newGeom;
@@ -1829,25 +1871,25 @@ QVector QgsGeometry::coerceToType( const Qgis::WkbType type, double
// Single -> multi
- if ( QgsWkbTypes::isMultiType( type ) && ! newGeom.isMultipart( ) )
+ if ( QgsWkbTypes::isMultiType( type ) && !newGeom.isMultipart() )
{
newGeom.convertToMultiType();
}
// Drop Z/M
- if ( newGeom.constGet()->is3D() && ! QgsWkbTypes::hasZ( type ) )
+ if ( newGeom.constGet()->is3D() && !QgsWkbTypes::hasZ( type ) )
{
newGeom.get()->dropZValue();
}
- if ( newGeom.constGet()->isMeasure() && ! QgsWkbTypes::hasM( type ) )
+ if ( newGeom.constGet()->isMeasure() && !QgsWkbTypes::hasM( type ) )
{
newGeom.get()->dropMValue();
}
// Add Z/M back, set to 0
- if ( ! newGeom.constGet()->is3D() && QgsWkbTypes::hasZ( type ) )
+ if ( !newGeom.constGet()->is3D() && QgsWkbTypes::hasZ( type ) )
{
newGeom.get()->addZValue( defaultZ );
}
- if ( ! newGeom.constGet()->isMeasure() && QgsWkbTypes::hasM( type ) )
+ if ( !newGeom.constGet()->isMeasure() && QgsWkbTypes::hasM( type ) )
{
newGeom.get()->addMValue( defaultM );
}
@@ -1859,11 +1901,11 @@ QVector QgsGeometry::coerceToType( const Qgis::WkbType type, double
}
// Multi -> single
- if ( ! QgsWkbTypes::isMultiType( type ) && newGeom.isMultipart( ) )
+ if ( !QgsWkbTypes::isMultiType( type ) && newGeom.isMultipart() )
{
const QgsGeometryCollection *parts( static_cast< const QgsGeometryCollection * >( newGeom.constGet() ) );
res.reserve( parts->partCount() );
- for ( int i = 0; i < parts->partCount( ); i++ )
+ for ( int i = 0; i < parts->partCount(); i++ )
{
res << QgsGeometry( parts->geometryN( i )->clone() );
}
@@ -1950,7 +1992,7 @@ bool QgsGeometry::convertToMultiType()
return true;
}
- std::unique_ptr< QgsAbstractGeometry >geom = QgsGeometryFactory::geomFromWkbType( QgsWkbTypes::multiType( d->geometry->wkbType() ) );
+ std::unique_ptr< QgsAbstractGeometry > geom = QgsGeometryFactory::geomFromWkbType( QgsWkbTypes::multiType( d->geometry->wkbType() ) );
QgsGeometryCollection *multiGeom = qgsgeometry_cast( geom.get() );
if ( !multiGeom )
{
@@ -1990,7 +2032,7 @@ bool QgsGeometry::convertToCurvedMultiType()
break;
}
- std::unique_ptr< QgsAbstractGeometry >geom = QgsGeometryFactory::geomFromWkbType( QgsWkbTypes::curveType( QgsWkbTypes::multiType( d->geometry->wkbType() ) ) );
+ std::unique_ptr< QgsAbstractGeometry > geom = QgsGeometryFactory::geomFromWkbType( QgsWkbTypes::curveType( QgsWkbTypes::multiType( d->geometry->wkbType() ) ) );
QgsGeometryCollection *multiGeom = qgsgeometry_cast( geom.get() );
if ( !multiGeom )
{
@@ -2099,8 +2141,8 @@ QgsPolylineXY QgsGeometry::asPolyline() const
return polyLine;
}
- bool doSegmentation = ( QgsWkbTypes::flatType( d->geometry->wkbType() ) == Qgis::WkbType::CompoundCurve
- || QgsWkbTypes::flatType( d->geometry->wkbType() ) == Qgis::WkbType::CircularString );
+ const Qgis::WkbType flatType = QgsWkbTypes::flatType( d->geometry->wkbType() );
+ bool doSegmentation = ( flatType == Qgis::WkbType::CompoundCurve || flatType == Qgis::WkbType::CircularString || flatType == Qgis::WkbType::NurbsCurve );
std::unique_ptr< QgsLineString > segmentizedLine;
QgsLineString *line = nullptr;
if ( doSegmentation )
@@ -2591,8 +2633,7 @@ QgsGeometry QgsGeometry::singleSidedBuffer( double distance, int segments, Qgis:
{
QgsGeos geos( d->geometry.get() );
mLastError.clear();
- std::unique_ptr< QgsAbstractGeometry > bufferGeom = geos.singleSidedBuffer( distance, segments, side,
- joinStyle, miterLimit, &mLastError );
+ std::unique_ptr< QgsAbstractGeometry > bufferGeom = geos.singleSidedBuffer( distance, segments, side, joinStyle, miterLimit, &mLastError );
if ( !bufferGeom )
{
QgsGeometry result;
@@ -2743,7 +2784,7 @@ QgsGeometry QgsGeometry::poleOfInaccessibility( double precision, double *distan
return engine.poleOfInaccessibility( precision, distanceToBoundary );
}
-QgsGeometry QgsGeometry::largestEmptyCircle( double tolerance, const QgsGeometry &boundary ) const
+QgsGeometry QgsGeometry::largestEmptyCircle( double tolerance, const QgsGeometry &boundary ) const
{
if ( !d->geometry )
{
@@ -2886,8 +2927,7 @@ QgsGeometry QgsGeometry::unionCoverage() const
return QgsGeometry();
}
- if ( QgsWkbTypes::flatType( d->geometry->wkbType() ) != Qgis::WkbType::GeometryCollection &&
- QgsWkbTypes::flatType( d->geometry->wkbType() ) != Qgis::WkbType::MultiPolygon
+ if ( QgsWkbTypes::flatType( d->geometry->wkbType() ) != Qgis::WkbType::GeometryCollection && QgsWkbTypes::flatType( d->geometry->wkbType() ) != Qgis::WkbType::MultiPolygon
&& QgsWkbTypes::flatType( d->geometry->wkbType() ) != Qgis::WkbType::Polygon )
return QgsGeometry();
@@ -3442,7 +3482,6 @@ Qgis::AngularDirection QgsGeometry::polygonOrientation() const
}
return Qgis::AngularDirection::NoOrientation;
-
}
QgsGeometry QgsGeometry::forcePolygonClockwise() const
@@ -3753,7 +3792,7 @@ void QgsGeometry::draw( QPainter &p ) const
static bool vertexIndexInfo( const QgsAbstractGeometry *g, int vertexIndex, int &partIndex, int &ringIndex, int &vertex )
{
if ( vertexIndex < 0 )
- return false; // clearly something wrong
+ return false; // clearly something wrong
if ( const QgsGeometryCollection *geomCollection = qgsgeometry_cast( g ) )
{
@@ -3875,7 +3914,7 @@ QString QgsGeometry::lastError() const
return mLastError;
}
-void QgsGeometry::filterVertices( const std::function &filter )
+void QgsGeometry::filterVertices( const std::function &filter )
{
if ( !d->geometry )
return;
@@ -3920,8 +3959,8 @@ void QgsGeometry::convertPolygon( const QgsPolygon &input, QgsPolygonXY &output
auto convertRing = []( const QgsCurve * ring ) -> QgsPolylineXY
{
QgsPolylineXY res;
- bool doSegmentation = ( QgsWkbTypes::flatType( ring->wkbType() ) == Qgis::WkbType::CompoundCurve
- || QgsWkbTypes::flatType( ring->wkbType() ) == Qgis::WkbType::CircularString );
+ const Qgis::WkbType flatType = QgsWkbTypes::flatType( ring->wkbType() );
+ bool doSegmentation = ( flatType == Qgis::WkbType::CompoundCurve || flatType == Qgis::WkbType::CircularString || flatType == Qgis::WkbType::NurbsCurve );
std::unique_ptr< QgsLineString > segmentizedLine;
const QgsLineString *line = nullptr;
if ( doSegmentation )
@@ -3972,7 +4011,7 @@ QgsGeometry QgsGeometry::fromQPointF( QPointF point )
QgsGeometry QgsGeometry::fromQPolygonF( const QPolygonF &polygon )
{
- std::unique_ptr < QgsLineString > ring( QgsLineString::fromQPolygonF( polygon ) );
+ std::unique_ptr< QgsLineString > ring( QgsLineString::fromQPolygonF( polygon ) );
if ( polygon.isClosed() )
{
@@ -4072,7 +4111,7 @@ QgsGeometry QgsGeometry::smooth( const unsigned int iterations, const double off
{
const QgsMultiLineString *inputMultiLine = qgsgeometry_cast< const QgsMultiLineString * >( geom.constGet() );
- auto resultMultiline = std::make_unique< QgsMultiLineString> ();
+ auto resultMultiline = std::make_unique< QgsMultiLineString>();
resultMultiline->reserve( inputMultiLine->numGeometries() );
for ( int i = 0; i < inputMultiLine->numGeometries(); ++i )
{
@@ -4106,9 +4145,7 @@ QgsGeometry QgsGeometry::smooth( const unsigned int iterations, const double off
}
}
-std::unique_ptr< QgsLineString > smoothCurve( const QgsLineString &line, const unsigned int iterations,
- const double offset, double squareDistThreshold, double maxAngleRads,
- bool isRing )
+std::unique_ptr< QgsLineString > smoothCurve( const QgsLineString &line, const unsigned int iterations, const double offset, double squareDistThreshold, double maxAngleRads, bool isRing )
{
auto result = std::make_unique< QgsLineString >( line );
QgsPointSequence outputLine;
@@ -4123,8 +4160,7 @@ std::unique_ptr< QgsLineString > smoothCurve( const QgsLineString &line, const u
QgsPoint p1 = result->pointN( result->numPoints() - 2 );
QgsPoint p2 = result->pointN( 0 );
QgsPoint p3 = result->pointN( 1 );
- double angle = QgsGeometryUtilsBase::angleBetweenThreePoints( p1.x(), p1.y(), p2.x(), p2.y(),
- p3.x(), p3.y() );
+ double angle = QgsGeometryUtilsBase::angleBetweenThreePoints( p1.x(), p1.y(), p2.x(), p2.y(), p3.x(), p3.y() );
angle = std::fabs( M_PI - angle );
skipFirst = angle > maxAngleRads;
}
@@ -4137,20 +4173,17 @@ std::unique_ptr< QgsLineString > smoothCurve( const QgsLineString &line, const u
if ( i == 0 && isRing )
{
QgsPoint p3 = result->pointN( result->numPoints() - 2 );
- angle = QgsGeometryUtilsBase::angleBetweenThreePoints( p1.x(), p1.y(), p2.x(), p2.y(),
- p3.x(), p3.y() );
+ angle = QgsGeometryUtilsBase::angleBetweenThreePoints( p1.x(), p1.y(), p2.x(), p2.y(), p3.x(), p3.y() );
}
else if ( i < result->numPoints() - 2 )
{
QgsPoint p3 = result->pointN( i + 2 );
- angle = QgsGeometryUtilsBase::angleBetweenThreePoints( p1.x(), p1.y(), p2.x(), p2.y(),
- p3.x(), p3.y() );
+ angle = QgsGeometryUtilsBase::angleBetweenThreePoints( p1.x(), p1.y(), p2.x(), p2.y(), p3.x(), p3.y() );
}
else if ( i == result->numPoints() - 2 && isRing )
{
QgsPoint p3 = result->pointN( 1 );
- angle = QgsGeometryUtilsBase::angleBetweenThreePoints( p1.x(), p1.y(), p2.x(), p2.y(),
- p3.x(), p3.y() );
+ angle = QgsGeometryUtilsBase::angleBetweenThreePoints( p1.x(), p1.y(), p2.x(), p2.y(), p3.x(), p3.y() );
}
skipLast = angle < M_PI - maxAngleRads || angle > M_PI + maxAngleRads;
@@ -4205,13 +4238,11 @@ std::unique_ptr QgsGeometry::smoothPolygon( const QgsPolygon &polygo
double squareDistThreshold = minimumDistance > 0 ? minimumDistance * minimumDistance : -1;
auto resultPoly = std::make_unique< QgsPolygon >();
- resultPoly->setExteriorRing( smoothCurve( *( static_cast< const QgsLineString *>( polygon.exteriorRing() ) ), iterations, offset,
- squareDistThreshold, maxAngleRads, true ).release() );
+ resultPoly->setExteriorRing( smoothCurve( *( static_cast< const QgsLineString *>( polygon.exteriorRing() ) ), iterations, offset, squareDistThreshold, maxAngleRads, true ).release() );
for ( int i = 0; i < polygon.numInteriorRings(); ++i )
{
- resultPoly->addInteriorRing( smoothCurve( *( static_cast< const QgsLineString *>( polygon.interiorRing( i ) ) ), iterations, offset,
- squareDistThreshold, maxAngleRads, true ).release() );
+ resultPoly->addInteriorRing( smoothCurve( *( static_cast< const QgsLineString *>( polygon.interiorRing( i ) ) ), iterations, offset, squareDistThreshold, maxAngleRads, true ).release() );
}
return resultPoly;
}
@@ -4224,8 +4255,7 @@ QgsGeometry QgsGeometry::convertToPoint( bool destMultipart ) const
{
bool srcIsMultipart = isMultipart();
- if ( ( destMultipart && srcIsMultipart ) ||
- ( !destMultipart && !srcIsMultipart ) )
+ if ( ( destMultipart && srcIsMultipart ) || ( !destMultipart && !srcIsMultipart ) )
{
// return a copy of the same geom
return QgsGeometry( *this );
@@ -4330,8 +4360,7 @@ QgsGeometry QgsGeometry::convertToLine( bool destMultipart ) const
{
bool srcIsMultipart = isMultipart();
- if ( ( destMultipart && srcIsMultipart ) ||
- ( !destMultipart && ! srcIsMultipart ) )
+ if ( ( destMultipart && srcIsMultipart ) || ( !destMultipart && !srcIsMultipart ) )
{
// return a copy of the same geom
return QgsGeometry( *this );
@@ -4504,8 +4533,7 @@ QgsGeometry QgsGeometry::convertToPolygon( bool destMultipart ) const
{
bool srcIsMultipart = isMultipart();
- if ( ( destMultipart && srcIsMultipart ) ||
- ( !destMultipart && ! srcIsMultipart ) )
+ if ( ( destMultipart && srcIsMultipart ) || ( !destMultipart && !srcIsMultipart ) )
{
// return a copy of the same geom
return QgsGeometry( *this );
@@ -4783,9 +4811,7 @@ QgsGeometry QgsGeometry::fillet( int vertexIndex, double radius, int segments )
QgsGeometry QgsGeometry::chamfer( const QgsPoint &segment1Start, const QgsPoint &segment1End, const QgsPoint &segment2Start, const QgsPoint &segment2End, double distance1, double distance2 )
{
- std::unique_ptr result( QgsGeometryUtils::createChamferGeometry(
- segment1Start, segment1End, segment2Start, segment2End, distance1, distance2
- ) );
+ std::unique_ptr result( QgsGeometryUtils::createChamferGeometry( segment1Start, segment1End, segment2Start, segment2End, distance1, distance2 ) );
if ( !result )
{
@@ -4797,9 +4823,7 @@ QgsGeometry QgsGeometry::chamfer( const QgsPoint &segment1Start, const QgsPoint
QgsGeometry QgsGeometry::fillet( const QgsPoint &segment1Start, const QgsPoint &segment1End, const QgsPoint &segment2Start, const QgsPoint &segment2End, double radius, int segments )
{
- std::unique_ptr result( QgsGeometryUtils::createFilletGeometry(
- segment1Start, segment1End, segment2Start, segment2End, radius, segments
- ) );
+ std::unique_ptr result( QgsGeometryUtils::createFilletGeometry( segment1Start, segment1End, segment2Start, segment2End, radius, segments ) );
if ( !result )
{
diff --git a/src/core/geometry/qgsgeometryfactory.cpp b/src/core/geometry/qgsgeometryfactory.cpp
index 160123d997fc..07fbaeb4bcd1 100644
--- a/src/core/geometry/qgsgeometryfactory.cpp
+++ b/src/core/geometry/qgsgeometryfactory.cpp
@@ -27,6 +27,7 @@
#include "qgsmultipoint.h"
#include "qgsmultipolygon.h"
#include "qgsmultisurface.h"
+#include "qgsnurbscurve.h"
#include "qgspoint.h"
#include "qgspolygon.h"
#include "qgspolyhedralsurface.h"
@@ -59,7 +60,7 @@ std::unique_ptr QgsGeometryFactory::geomFromWkb( QgsConstWk
{
try
{
- geom->fromWkb( wkbPtr ); // also updates wkbPtr
+ geom->fromWkb( wkbPtr ); // also updates wkbPtr
}
catch ( const QgsWkbException &e )
{
@@ -136,6 +137,10 @@ std::unique_ptr QgsGeometryFactory::geomFromWkt( const QStr
{
geom = std::make_unique< QgsTriangulatedSurface >();
}
+ else if ( trimmed.startsWith( QLatin1String( "NurbsCurve" ), Qt::CaseInsensitive ) )
+ {
+ geom = std::make_unique< QgsNurbsCurve >();
+ }
if ( geom )
{
@@ -271,6 +276,8 @@ std::unique_ptr QgsGeometryFactory::geomFromWkbType( Qgis::
return std::make_unique< QgsPolyhedralSurface >();
case Qgis::WkbType::TIN:
return std::make_unique< QgsTriangulatedSurface >();
+ case Qgis::WkbType::NurbsCurve:
+ return std::make_unique< QgsNurbsCurve >();
default:
return nullptr;
}
diff --git a/src/core/geometry/qgsgeometryutils.cpp b/src/core/geometry/qgsgeometryutils.cpp
index d2b21c7b1e0f..8b79965d0c82 100644
--- a/src/core/geometry/qgsgeometryutils.cpp
+++ b/src/core/geometry/qgsgeometryutils.cpp
@@ -538,6 +538,33 @@ QgsPoint QgsGeometryUtils::interpolatePointOnArc( const QgsPoint &pt1, const Qgs
return QgsPoint( pt1.wkbType(), x, y, z, m );
}
+QgsPoint QgsGeometryUtils::interpolatePointOnCubicBezier( const QgsPoint &p0, const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, double t )
+{
+ // Cubic Bézier formula: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃
+ const double t1 = 1.0 - t;
+ const double t1_2 = t1 * t1;
+ const double t1_3 = t1_2 * t1;
+ const double t_2 = t * t;
+ const double t_3 = t_2 * t;
+
+ const double x = t1_3 * p0.x() + 3.0 * t1_2 * t * p1.x() + 3.0 * t1 * t_2 * p2.x() + t_3 * p3.x();
+ const double y = t1_3 * p0.y() + 3.0 * t1_2 * t * p1.y() + 3.0 * t1 * t_2 * p2.y() + t_3 * p3.y();
+
+ double z = std::numeric_limits::quiet_NaN();
+ if ( p0.is3D() && p1.is3D() && p2.is3D() && p3.is3D() )
+ {
+ z = t1_3 * p0.z() + 3.0 * t1_2 * t * p1.z() + 3.0 * t1 * t_2 * p2.z() + t_3 * p3.z();
+ }
+
+ double m = std::numeric_limits::quiet_NaN();
+ if ( p0.isMeasure() && p1.isMeasure() && p2.isMeasure() && p3.isMeasure() )
+ {
+ m = t1_3 * p0.m() + 3.0 * t1_2 * t * p1.m() + 3.0 * t1 * t_2 * p2.m() + t_3 * p3.m();
+ }
+
+ return QgsPoint( p0.wkbType(), x, y, z, m );
+}
+
bool QgsGeometryUtils::segmentMidPoint( const QgsPoint &p1, const QgsPoint &p2, QgsPoint &result, double radius, const QgsPoint &mousePos )
{
const QgsPoint midPoint( ( p1.x() + p2.x() ) / 2.0, ( p1.y() + p2.y() ) / 2.0 );
diff --git a/src/core/geometry/qgsgeometryutils.h b/src/core/geometry/qgsgeometryutils.h
index 907425f54db2..2d9c7a80e3ba 100644
--- a/src/core/geometry/qgsgeometryutils.h
+++ b/src/core/geometry/qgsgeometryutils.h
@@ -235,6 +235,25 @@ class CORE_EXPORT QgsGeometryUtils
*/
static QgsPoint interpolatePointOnArc( const QgsPoint &pt1, const QgsPoint &pt2, const QgsPoint &pt3, double distance ) SIP_HOLDGIL;
+ /**
+ * Evaluates a point on a cubic Bézier curve defined by four control points.
+ *
+ * \param p0 start point (the curve passes through this point)
+ * \param p1 first control point
+ * \param p2 second control point
+ * \param p3 end point (the curve passes through this point)
+ * \param t parameter value between 0 and 1
+ *
+ * \returns the point on the Bézier curve at parameter \a t
+ *
+ * Any Z or M values present in the input points will also be interpolated.
+ *
+ * The cubic Bézier formula is: B(t) = (1-t)³P₀ + 3(1-t)²tP₁ + 3(1-t)t²P₂ + t³P₃
+ *
+ * \since QGIS 4.0
+ */
+ static QgsPoint interpolatePointOnCubicBezier( const QgsPoint &p0, const QgsPoint &p1, const QgsPoint &p2, const QgsPoint &p3, double t ) SIP_HOLDGIL;
+
/**
* Calculates midpoint on circle passing through \a p1 and \a p2, closest to
* the given coordinate \a mousePos. Z dimension is supported and is retrieved from the
diff --git a/src/core/geometry/qgslinestring.cpp b/src/core/geometry/qgslinestring.cpp
index e1353c97d4dd..7a10e0719f07 100644
--- a/src/core/geometry/qgslinestring.cpp
+++ b/src/core/geometry/qgslinestring.cpp
@@ -179,71 +179,22 @@ QgsLineString::QgsLineString( const QgsLineSegment2D &segment )
mY[1] = segment.endY();
}
-static double cubicInterpolate( double a, double b,
- double A, double B, double C, double D )
-{
- return A * b * b * b + 3 * B * b * b * a + 3 * C * b * a * a + D * a * a * a;
-}
-
std::unique_ptr< QgsLineString > QgsLineString::fromBezierCurve( const QgsPoint &start, const QgsPoint &controlPoint1, const QgsPoint &controlPoint2, const QgsPoint &end, int segments )
{
if ( segments == 0 )
return std::make_unique< QgsLineString >();
- QVector x;
- x.resize( segments + 1 );
- QVector y;
- y.resize( segments + 1 );
- QVector z;
- double *zData = nullptr;
- if ( start.is3D() && end.is3D() && controlPoint1.is3D() && controlPoint2.is3D() )
- {
- z.resize( segments + 1 );
- zData = z.data();
- }
- QVector m;
- double *mData = nullptr;
- if ( start.isMeasure() && end.isMeasure() && controlPoint1.isMeasure() && controlPoint2.isMeasure() )
- {
- m.resize( segments + 1 );
- mData = m.data();
- }
+ QgsPointSequence points;
+ points.reserve( segments + 1 );
- double *xData = x.data();
- double *yData = y.data();
const double step = 1.0 / segments;
- double a = 0;
- double b = 1.0;
- for ( int i = 0; i < segments; i++, a += step, b -= step )
+ for ( int i = 0; i <= segments; ++i )
{
- if ( i == 0 )
- {
- *xData++ = start.x();
- *yData++ = start.y();
- if ( zData )
- *zData++ = start.z();
- if ( mData )
- *mData++ = start.m();
- }
- else
- {
- *xData++ = cubicInterpolate( a, b, start.x(), controlPoint1.x(), controlPoint2.x(), end.x() );
- *yData++ = cubicInterpolate( a, b, start.y(), controlPoint1.y(), controlPoint2.y(), end.y() );
- if ( zData )
- *zData++ = cubicInterpolate( a, b, start.z(), controlPoint1.z(), controlPoint2.z(), end.z() );
- if ( mData )
- *mData++ = cubicInterpolate( a, b, start.m(), controlPoint1.m(), controlPoint2.m(), end.m() );
- }
+ const double t = i * step;
+ points.append( QgsGeometryUtils::interpolatePointOnCubicBezier( start, controlPoint1, controlPoint2, end, t ) );
}
- *xData = end.x();
- *yData = end.y();
- if ( zData )
- *zData = end.z();
- if ( mData )
- *mData = end.m();
-
- return std::make_unique< QgsLineString >( x, y, z, m );
+ return std::make_unique< QgsLineString >( points );
}
std::unique_ptr< QgsLineString > QgsLineString::fromQPolygonF( const QPolygonF &polygon )
diff --git a/src/core/geometry/qgsnurbscurve.cpp b/src/core/geometry/qgsnurbscurve.cpp
new file mode 100644
index 000000000000..f277150e1db4
--- /dev/null
+++ b/src/core/geometry/qgsnurbscurve.cpp
@@ -0,0 +1,1766 @@
+/***************************************************************************
+ qgsnurbscurve.cpp
+ -----------------
+ begin : September 2025
+ copyright : (C) 2025 by Loïc Bartoletti
+ email : loic dot bartoletti at oslandia dot com
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#include "qgsnurbscurve.h"
+
+#include
+#include
+#include
+#include
+#include
+
+#include "qgsapplication.h"
+#include "qgsbox3d.h"
+#include "qgscoordinatetransform.h"
+#include "qgsfeedback.h"
+#include "qgsgeometrytransformer.h"
+#include "qgsgeometryutils.h"
+#include "qgsgeometryutils_base.h"
+#include "qgslinestring.h"
+#include "qgspoint.h"
+#include "qgsrectangle.h"
+#include "qgswkbptr.h"
+#include "qgswkbtypes.h"
+
+#include
+
+using namespace nlohmann;
+
+QgsNurbsCurve::QgsNurbsCurve()
+{
+ mWkbType = Qgis::WkbType::NurbsCurve;
+}
+
+QgsNurbsCurve::QgsNurbsCurve( const QVector &ctrlPoints, int degree, const QVector &knots, const QVector &weights )
+ : mControlPoints( ctrlPoints )
+ , mKnots( knots )
+ , mWeights( weights )
+ , mDegree( degree )
+{
+ mWkbType = Qgis::WkbType::NurbsCurve;
+
+ // Update WKB type based on coordinate dimensions
+ if ( !mControlPoints.isEmpty() )
+ {
+ const QgsPoint &firstPoint = mControlPoints.first();
+ if ( firstPoint.is3D() )
+ mWkbType = QgsWkbTypes::addZ( mWkbType );
+ if ( firstPoint.isMeasure() )
+ mWkbType = QgsWkbTypes::addM( mWkbType );
+ }
+}
+
+QgsCurve *QgsNurbsCurve::clone() const
+{
+ return new QgsNurbsCurve( *this );
+}
+
+bool QgsNurbsCurve::isBezier() const
+{
+ const int n = mControlPoints.size();
+ if ( n < 2 || mDegree < 1 )
+ return false;
+
+ if ( mDegree != n - 1 )
+ return false;
+
+ if ( !isBSpline() )
+ return false;
+
+ if ( mKnots.size() != n + mDegree + 1 )
+ return false;
+
+ for ( int i = 0; i <= mDegree; ++i )
+ {
+ if ( !qgsDoubleNear( mKnots[i], 0.0 ) )
+ return false;
+ }
+ for ( int i = n; i < mKnots.size(); ++i )
+ {
+ if ( !qgsDoubleNear( mKnots[i], 1.0 ) )
+ return false;
+ }
+
+ return true;
+}
+
+bool QgsNurbsCurve::isBSpline() const
+{
+ for ( const double w : mWeights )
+ {
+ if ( !qgsDoubleNear( w, 1.0 ) )
+ return false;
+ }
+ return true;
+}
+
+bool QgsNurbsCurve::isRational() const
+{
+ return !isBSpline();
+}
+
+bool QgsNurbsCurve::isPolyBezier() const
+{
+ const int n = mControlPoints.size();
+ return mDegree == 3 && n >= 4 && ( n - 1 ) % 3 == 0;
+}
+
+/**
+ * \brief Find the knot span index for parameter u using binary search.
+ *
+ * This is Algorithm A2.1 from "The NURBS Book" (Piegl & Tiller).
+ * Returns the index i such that knots[i] <= u < knots[i+1].
+ * For a NURBS curve with n control points and degree p, valid spans
+ * are in range [p, n-1].
+ *
+ * \param degree polynomial degree of the NURBS curve
+ * \param u parameter value to locate
+ * \param knots knot vector
+ * \param nPoints number of control points
+ * \returns knot span index
+ */
+static int findKnotSpan( const int degree, const double u, const QVector &knots, const int nPoints )
+{
+ // Special case: u at or beyond end of parameter range
+ if ( u >= knots[nPoints] )
+ return nPoints - 1;
+
+ // Special case: u at or before start of parameter range
+ if ( u <= knots[degree] )
+ return degree;
+
+ // Binary search for the knot span
+ int low = degree;
+ int high = nPoints;
+ int mid = ( low + high ) / 2;
+
+ while ( u < knots[mid] || u >= knots[mid + 1] )
+ {
+ if ( u < knots[mid] )
+ high = mid;
+ else
+ low = mid;
+ mid = ( low + high ) / 2;
+ }
+
+ return mid;
+}
+
+// Evaluates the NURBS curve at parameter t using De Boor's algorithm.
+// Implements Algorithm A4.1 from "The NURBS Book" (Piegl & Tiller).
+QgsPoint QgsNurbsCurve::evaluate( double t ) const
+{
+ const int n = mControlPoints.size();
+ if ( n == 0 )
+ {
+ return QgsPoint();
+ }
+
+ QString error;
+ if ( !isValid( error, Qgis::GeometryValidityFlags() ) )
+ {
+ return QgsPoint();
+ }
+
+ // Clamp parameter t to valid range [0,1]
+ if ( t <= 0.0 )
+ return mControlPoints.first();
+ if ( t >= 1.0 )
+ return mControlPoints.last();
+
+ const bool hasZ = !mControlPoints.isEmpty() && mControlPoints.first().is3D();
+ const bool hasM = !mControlPoints.isEmpty() && mControlPoints.first().isMeasure();
+
+ // Remap parameter from [0,1] to knot vector range [knots[degree], knots[n]]
+ const double u = mKnots[mDegree] + t * ( mKnots[n] - mKnots[mDegree] );
+
+ // Find the knot span containing parameter u (Algorithm A2.1)
+ const int span = findKnotSpan( mDegree, u, mKnots, n );
+
+ // Temporary arrays for De Boor iteration (degree+1 points)
+ // Using homogeneous coordinates: (w*x, w*y, w*z, w) for rational curves
+ QVector tempX( mDegree + 1 );
+ QVector tempY( mDegree + 1 );
+ QVector tempZ( mDegree + 1 );
+ QVector tempM( mDegree + 1 );
+ QVector tempW( mDegree + 1 );
+
+ // Initialize temp arrays with control points and weights
+ for ( int j = 0; j <= mDegree; ++j )
+ {
+ const int cpIdx = span - mDegree + j;
+ const QgsPoint &cp = mControlPoints[cpIdx];
+ const double w = ( cpIdx < mWeights.size() ) ? mWeights[cpIdx] : 1.0;
+
+ // Store in homogeneous coordinates (w * P)
+ tempX[j] = cp.x() * w;
+ tempY[j] = cp.y() * w;
+ tempZ[j] = hasZ ? cp.z() * w : 0.0;
+ tempM[j] = hasM ? cp.m() : 0.0; // M is not weighted
+ tempW[j] = w;
+ }
+
+ // De Boor iteration (Algorithm A4.1) in homogeneous space
+ for ( int k = 1; k <= mDegree; ++k )
+ {
+ for ( int j = mDegree; j >= k; --j )
+ {
+ const int knotIdx = span - mDegree + j;
+ const double denom = mKnots[knotIdx + mDegree - k + 1] - mKnots[knotIdx];
+
+ if ( !qgsDoubleNear( denom, 0.0 ) )
+ {
+ const double alpha = ( u - mKnots[knotIdx] ) / denom;
+ const double oneMinusAlpha = 1.0 - alpha;
+
+ // Linear interpolation in homogeneous space
+ tempX[j] = oneMinusAlpha * tempX[j - 1] + alpha * tempX[j];
+ tempY[j] = oneMinusAlpha * tempY[j - 1] + alpha * tempY[j];
+ if ( hasZ )
+ tempZ[j] = oneMinusAlpha * tempZ[j - 1] + alpha * tempZ[j];
+ if ( hasM )
+ tempM[j] = oneMinusAlpha * tempM[j - 1] + alpha * tempM[j];
+
+ // Interpolate weights
+ tempW[j] = oneMinusAlpha * tempW[j - 1] + alpha * tempW[j];
+ }
+ }
+ }
+
+ // Result is in temp[degree], stored in homogeneous coordinates
+ // Project back to Cartesian by dividing by weight
+ double x = tempX[mDegree];
+ double y = tempY[mDegree];
+ double z = tempZ[mDegree];
+ double m = tempM[mDegree];
+ const double w = tempW[mDegree];
+
+ if ( !qgsDoubleNear( w, 0.0 ) && !qgsDoubleNear( w, 1.0 ) )
+ {
+ x /= w;
+ y /= w;
+ if ( hasZ )
+ z /= w;
+ // M is not divided by weight (it's not in homogeneous space)
+ }
+
+ // Create point with appropriate dimensionality
+ if ( hasZ && hasM )
+ return QgsPoint( x, y, z, m );
+ else if ( hasZ )
+ return QgsPoint( x, y, z );
+ else if ( hasM )
+ return QgsPoint( x, y, std::numeric_limits::quiet_NaN(), m );
+ else
+ return QgsPoint( x, y );
+}
+
+bool QgsNurbsCurve::isClosed() const
+{
+ if ( mControlPoints.size() < 2 )
+ return false;
+
+ // Check if curve endpoints are the same by evaluating at t=0 and t=1
+ const QgsPoint startPt = evaluate( 0.0 );
+ const QgsPoint endPt = evaluate( 1.0 );
+
+ bool closed = qgsDoubleNear( startPt.x(), endPt.x() ) && qgsDoubleNear( startPt.y(), endPt.y() );
+
+ if ( is3D() && closed )
+ closed &= qgsDoubleNear( startPt.z(), endPt.z() ) || ( std::isnan( startPt.z() ) && std::isnan( endPt.z() ) );
+
+ return closed;
+}
+
+bool QgsNurbsCurve::isClosed2D() const
+{
+ if ( mControlPoints.size() < 2 )
+ return false;
+
+ // Check if curve endpoints are the same in 2D
+ const QgsPoint startPt = evaluate( 0.0 );
+ const QgsPoint endPt = evaluate( 1.0 );
+
+ return qgsDoubleNear( startPt.x(), endPt.x() ) && qgsDoubleNear( startPt.y(), endPt.y() );
+}
+
+QgsLineString *QgsNurbsCurve::curveToLine( double tolerance, SegmentationToleranceType toleranceType ) const
+{
+ Q_UNUSED( toleranceType );
+
+ // Determine number of segments based on tolerance (angular approximation)
+ // For NURBS curves, we use uniform parameterization as a first approximation
+ const int steps = std::max( 2, static_cast( 2 * M_PI / tolerance ) );
+
+ auto line = new QgsLineString();
+ for ( int i = 0; i <= steps; ++i )
+ {
+ const double t = static_cast( i ) / steps;
+ const QgsPoint pt = evaluate( t );
+ line->addVertex( pt );
+ }
+
+ return line;
+}
+
+void QgsNurbsCurve::draw( QPainter &p ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( line )
+ line->draw( p );
+}
+
+void QgsNurbsCurve::drawAsPolygon( QPainter &p ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( line )
+ line->drawAsPolygon( p );
+}
+
+QPolygonF QgsNurbsCurve::asQPolygonF() const
+{
+ std::unique_ptr line( curveToLine() );
+ return line ? line->asQPolygonF() : QPolygonF();
+}
+
+QgsPoint QgsNurbsCurve::endPoint() const
+{
+ return mControlPoints.isEmpty() ? QgsPoint() : mControlPoints.last();
+}
+
+bool QgsNurbsCurve::equals( const QgsCurve &other ) const
+{
+ if ( geometryType() != other.geometryType() )
+ {
+ return false;
+ }
+
+ const QgsNurbsCurve *o = qgsgeometry_cast( &other );
+ if ( !o )
+ return false;
+
+ if ( o->mDegree != mDegree )
+ {
+ return false;
+ }
+
+ if ( mControlPoints != o->mControlPoints )
+ return false;
+
+ if ( mWeights != o->mWeights )
+ return false;
+
+ if ( mKnots != o->mKnots )
+ return false;
+
+ return true;
+}
+
+int QgsNurbsCurve::indexOf( const QgsPoint &point ) const
+{
+ for ( int i = 0; i < mControlPoints.size(); ++i )
+ {
+ if ( qgsDoubleNear( mControlPoints[i].distance( point ), 0.0 ) )
+ {
+ return i;
+ }
+ }
+ return -1;
+}
+
+QgsPoint *QgsNurbsCurve::interpolatePoint( double distance ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ {
+ return nullptr;
+ }
+ return line->interpolatePoint( distance );
+}
+
+int QgsNurbsCurve::numPoints() const
+{
+ return mControlPoints.size();
+}
+
+bool QgsNurbsCurve::pointAt( int node, QgsPoint &point, Qgis::VertexType &type ) const
+{
+ if ( node < 0 || node >= mControlPoints.size() )
+ {
+ return false;
+ }
+ point = mControlPoints[node];
+ type = Qgis::VertexType::ControlPoint;
+ return true;
+}
+
+void QgsNurbsCurve::points( QgsPointSequence &pts ) const
+{
+ for ( const QgsPoint &p : mControlPoints )
+ {
+ pts.append( p );
+ }
+}
+
+QgsCurve *QgsNurbsCurve::reversed() const
+{
+ auto rev = new QgsNurbsCurve( *this );
+ std::reverse( rev->mControlPoints.begin(), rev->mControlPoints.end() );
+ std::reverse( rev->mWeights.begin(), rev->mWeights.end() );
+
+ // Reverse and remap knot vector: new_knot[i] = max_knot + min_knot - old_knot[n-1-i]
+ if ( !rev->mKnots.isEmpty() )
+ {
+ const double maxKnot = rev->mKnots.last();
+ const double minKnot = rev->mKnots.first();
+ std::reverse( rev->mKnots.begin(), rev->mKnots.end() );
+ for ( double &knot : rev->mKnots )
+ {
+ knot = maxKnot + minKnot - knot;
+ }
+ }
+
+ return rev;
+}
+
+void QgsNurbsCurve::scroll( int firstVertexIndex )
+{
+ // Scrolling only makes sense for closed curves
+ if ( !isClosed() || firstVertexIndex <= 0 || firstVertexIndex >= mControlPoints.size() )
+ {
+ return;
+ }
+
+ // Rotate control points and weights
+ std::rotate( mControlPoints.begin(), mControlPoints.begin() + firstVertexIndex, mControlPoints.end() );
+ std::rotate( mWeights.begin(), mWeights.begin() + firstVertexIndex, mWeights.end() );
+
+ // Rotate knot vector and adjust values to preserve parameter domain
+ if ( !mKnots.isEmpty() && firstVertexIndex < mKnots.size() )
+ {
+ const double delta = mKnots[firstVertexIndex] - mKnots[0];
+ std::rotate( mKnots.begin(), mKnots.begin() + firstVertexIndex, mKnots.end() );
+ // Shift all knot values by -delta to preserve the start parameter
+ for ( double &knot : mKnots )
+ {
+ knot -= delta;
+ }
+ }
+
+ clearCache();
+}
+
+std::tuple, std::unique_ptr>
+ QgsNurbsCurve::splitCurveAtVertex( int index ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ {
+ return std::make_tuple( nullptr, nullptr );
+ }
+ return line->splitCurveAtVertex( index );
+}
+
+QgsPoint QgsNurbsCurve::startPoint() const
+{
+ return mControlPoints.isEmpty() ? QgsPoint() : mControlPoints.first();
+}
+
+void QgsNurbsCurve::sumUpArea( double &sum ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( line )
+ line->sumUpArea( sum );
+}
+
+double QgsNurbsCurve::xAt( int index ) const
+{
+ if ( index < 0 || index >= mControlPoints.size() )
+ return 0.0;
+ return mControlPoints[index].x();
+}
+
+double QgsNurbsCurve::yAt( int index ) const
+{
+ if ( index < 0 || index >= mControlPoints.size() )
+ return 0.0;
+ return mControlPoints[index].y();
+}
+
+double QgsNurbsCurve::zAt( int index ) const
+{
+ if ( index < 0 || index >= mControlPoints.size() )
+ return 0.0;
+ return mControlPoints[index].is3D() ? mControlPoints[index].z() : std::numeric_limits::quiet_NaN();
+}
+
+double QgsNurbsCurve::mAt( int index ) const
+{
+ if ( index < 0 || index >= mControlPoints.size() )
+ return 0.0;
+ return mControlPoints[index].isMeasure() ? mControlPoints[index].m() : std::numeric_limits::quiet_NaN();
+}
+
+bool QgsNurbsCurve::addZValue( double zValue )
+{
+ if ( QgsWkbTypes::hasZ( mWkbType ) )
+ return false;
+
+ clearCache();
+ mWkbType = QgsWkbTypes::addZ( mWkbType );
+
+ for ( QgsPoint &p : mControlPoints )
+ {
+ p.addZValue( zValue );
+ }
+
+ return true;
+}
+
+bool QgsNurbsCurve::addMValue( double mValue )
+{
+ if ( QgsWkbTypes::hasM( mWkbType ) )
+ return false;
+
+ clearCache();
+ mWkbType = QgsWkbTypes::addM( mWkbType );
+
+ for ( QgsPoint &p : mControlPoints )
+ {
+ p.addMValue( mValue );
+ }
+
+ return true;
+}
+
+bool QgsNurbsCurve::dropZValue()
+{
+ if ( !is3D() )
+ return false;
+
+ for ( QgsPoint &p : mControlPoints )
+ {
+ p.setZ( std::numeric_limits::quiet_NaN() );
+ }
+
+ mWkbType = QgsWkbTypes::dropZ( mWkbType );
+ clearCache();
+ return true;
+}
+
+bool QgsNurbsCurve::dropMValue()
+{
+ if ( !isMeasure() )
+ return false;
+
+ for ( QgsPoint &p : mControlPoints )
+ {
+ p.setM( std::numeric_limits::quiet_NaN() );
+ }
+
+ mWkbType = QgsWkbTypes::dropM( mWkbType );
+ clearCache();
+ return true;
+}
+
+bool QgsNurbsCurve::deleteVertex( QgsVertexId position )
+{
+ if ( position.part != 0 || position.ring != 0 )
+ {
+ return false;
+ }
+ const int idx = position.vertex;
+ if ( idx < 0 || idx >= mControlPoints.size() )
+ {
+ return false;
+ }
+ mControlPoints.remove( idx );
+ if ( idx < mWeights.size() )
+ {
+ mWeights.remove( idx );
+ }
+
+ const int n = mControlPoints.size();
+ const int knotsSize = n + mDegree + 1;
+ mKnots.clear();
+ mKnots.reserve( knotsSize );
+ for ( int i = 0; i < knotsSize; ++i )
+ {
+ if ( i <= mDegree )
+ mKnots.append( 0.0 );
+ else if ( i >= n )
+ mKnots.append( 1.0 );
+ else
+ mKnots.append( static_cast( i - mDegree ) / ( n - mDegree ) );
+ }
+
+ clearCache();
+ return true;
+}
+
+void QgsNurbsCurve::filterVertices( const std::function &filter )
+{
+ QVector newPts;
+ QVector newWeights;
+ for ( int i = 0; i < mControlPoints.size(); ++i )
+ {
+ if ( filter( mControlPoints[i] ) )
+ {
+ newPts.append( mControlPoints[i] );
+ if ( i < mWeights.size() )
+ newWeights.append( mWeights[i] );
+ }
+ }
+ mControlPoints = newPts;
+ mWeights = newWeights;
+
+ const int n = mControlPoints.size();
+ const int knotsSize = n + mDegree + 1;
+ mKnots.clear();
+ mKnots.reserve( knotsSize );
+ for ( int i = 0; i < knotsSize; ++i )
+ {
+ if ( i <= mDegree )
+ mKnots.append( 0.0 );
+ else if ( i >= n )
+ mKnots.append( 1.0 );
+ else
+ mKnots.append( static_cast( i - mDegree ) / ( n - mDegree ) );
+ }
+
+ clearCache();
+}
+
+bool QgsNurbsCurve::fromWkb( QgsConstWkbPtr &wkb )
+{
+ clear();
+
+ if ( !wkb )
+ return false;
+
+ Qgis::WkbType type = wkb.readHeader();
+ if ( QgsWkbTypes::flatType( type ) != Qgis::WkbType::NurbsCurve )
+ return false;
+
+ mWkbType = type;
+ const bool is3D = QgsWkbTypes::hasZ( type );
+ const bool isMeasure = QgsWkbTypes::hasM( type );
+
+ // Read degree (4 bytes uint32)
+ quint32 degree;
+ wkb >> degree;
+
+ // Validate degree before casting to int
+ if ( degree < 1 || degree > static_cast( std::numeric_limits::max() ) )
+ return false;
+
+ mDegree = static_cast( degree );
+
+ // Read number of control points (4 bytes uint32)
+ quint32 numControlPoints;
+ wkb >> numControlPoints;
+
+ // Sanity check: numControlPoints should be reasonable given the WKB blob size
+ // Each control point needs at least:
+ // - 1 byte (endianness)
+ // - 16 bytes (x,y)
+ // - 8 bytes (z) if 3D
+ // - 8 bytes (m) if measure
+ // - 1 byte (weight flag)
+ // Minimum: 18 bytes (2D) to 34 bytes (ZM)
+ const int minBytesPerPoint = 18 + ( is3D ? 8 : 0 ) + ( isMeasure ? 8 : 0 );
+ if ( numControlPoints > static_cast( wkb.remaining() / minBytesPerPoint + 1 ) )
+ return false;
+
+ mControlPoints.clear();
+ mWeights.clear();
+ mControlPoints.reserve( numControlPoints );
+ mWeights.reserve( numControlPoints );
+
+ // Read control points
+ for ( quint32 i = 0; i < numControlPoints; ++i )
+ {
+ // Read byte order for this point (1 byte)
+ char pointEndianness;
+ wkb >> pointEndianness;
+
+ // Validate endianness matches the WKB header (must be same)
+ // The endianness should be 0 (big-endian) or 1 (little-endian)
+ if ( pointEndianness != 0 && pointEndianness != 1 )
+ return false;
+
+ // Read coordinates
+ double x, y, z = 0.0, m = 0.0;
+ wkb >> x >> y;
+
+ if ( is3D )
+ wkb >> z;
+ if ( isMeasure )
+ wkb >> m;
+
+ // Read weight flag (1 byte)
+ char weightFlag;
+ wkb >> weightFlag;
+
+ double weight = 1.0;
+ if ( weightFlag == 1 )
+ {
+ // Read custom weight (8 bytes double)
+ wkb >> weight;
+ }
+
+ // Create point with appropriate dimensionality
+ QgsPoint point;
+ if ( is3D && isMeasure )
+ point = QgsPoint( x, y, z, m );
+ else if ( is3D )
+ point = QgsPoint( x, y, z );
+ else if ( isMeasure )
+ point = QgsPoint( x, y, std::numeric_limits::quiet_NaN(), m );
+ else
+ point = QgsPoint( x, y );
+
+ mControlPoints.append( point );
+ mWeights.append( weight );
+ }
+
+ // Read number of knots (4 bytes uint32)
+ quint32 numKnots;
+ wkb >> numKnots;
+
+ // Sanity check: numKnots should be numControlPoints + degree + 1
+ const quint32 expectedKnots = numControlPoints + degree + 1;
+ if ( numKnots != expectedKnots )
+ return false;
+
+ // Sanity check: remaining WKB should have enough bytes for knots
+ if ( numKnots * sizeof( double ) > static_cast( wkb.remaining() ) )
+ return false;
+
+ mKnots.clear();
+ mKnots.reserve( numKnots );
+
+ // Read knot values (8 bytes double each)
+ for ( quint32 i = 0; i < numKnots; ++i )
+ {
+ double knot;
+ wkb >> knot;
+ mKnots.append( knot );
+ }
+
+ return true;
+}
+
+bool QgsNurbsCurve::fromWkt( const QString &wkt )
+{
+ clear();
+
+ const QString geomTypeStr = wkt.split( '(' )[0].trimmed().toUpper();
+
+ if ( !geomTypeStr.startsWith( QLatin1String( "NURBSCURVE" ) ) )
+ {
+ return false;
+ }
+
+ // Determine dimensionality from the geometry type string
+ if ( geomTypeStr.contains( QLatin1String( "ZM" ) ) )
+ mWkbType = Qgis::WkbType::NurbsCurveZM;
+ else if ( geomTypeStr.endsWith( QLatin1String( " Z" ) ) || geomTypeStr == QLatin1String( "NURBSCURVE Z" ) )
+ mWkbType = Qgis::WkbType::NurbsCurveZ;
+ else if ( geomTypeStr.endsWith( QLatin1String( " M" ) ) || geomTypeStr == QLatin1String( "NURBSCURVE M" ) )
+ mWkbType = Qgis::WkbType::NurbsCurveM;
+ else
+ mWkbType = Qgis::WkbType::NurbsCurve;
+
+ QPair parts = QgsGeometryUtils::wktReadBlock( wkt );
+
+ if ( parts.second.compare( QLatin1String( "EMPTY" ), Qt::CaseInsensitive ) == 0 || parts.second.isEmpty() )
+ return true;
+
+ // Split the content by commas at parentheses level 0
+ QStringList blocks = QgsGeometryUtils::wktGetChildBlocks( parts.second, QString() );
+
+ if ( blocks.isEmpty() )
+ return false;
+
+ // First block should be the degree
+ bool ok = true;
+ int degree = blocks[0].trimmed().toInt( &ok );
+ if ( !ok || degree < 1 )
+ return false;
+
+ if ( blocks.size() < 2 )
+ return false;
+
+ // Second block should be the control points
+ QString pointsStr = blocks[1].trimmed();
+
+ // Validate control points block starts with '(' and ends with ')'
+ if ( !pointsStr.startsWith( QLatin1Char( '(' ) ) || !pointsStr.endsWith( QLatin1Char( ')' ) ) )
+ return false;
+
+ pointsStr = pointsStr.mid( 1, pointsStr.length() - 2 ).trimmed();
+
+ // Parse control points
+ QStringList pointsCoords = pointsStr.split( ',', Qt::SkipEmptyParts );
+ QVector controlPoints;
+
+ const thread_local QRegularExpression rx( QStringLiteral( "\\s+" ) );
+
+ for ( const QString &pointStr : pointsCoords )
+ {
+ QStringList coords = pointStr.trimmed().split( rx, Qt::SkipEmptyParts );
+
+ if ( coords.size() < 2 )
+ return false;
+
+ QgsPoint point;
+ bool ok = true;
+
+ double x = coords[0].toDouble( &ok );
+ if ( !ok )
+ return false;
+
+ double y = coords[1].toDouble( &ok );
+ if ( !ok )
+ return false;
+
+ // Handle different coordinate patterns based on declared geometry type
+ if ( coords.size() >= 3 )
+ {
+ if ( isMeasure() && !is3D() && coords.size() == 3 )
+ {
+ // NURBSCURVE M pattern: (x y m) - third coordinate is M, not Z
+ double m = coords[2].toDouble( &ok );
+ if ( !ok )
+ return false;
+ point = QgsPoint( x, y, std::numeric_limits::quiet_NaN(), m );
+ }
+ else if ( is3D() && !isMeasure() && coords.size() >= 3 )
+ {
+ // NURBSCURVE Z pattern: (x y z)
+ double z = coords[2].toDouble( &ok );
+ if ( !ok )
+ return false;
+ point = QgsPoint( x, y, z );
+ }
+ else if ( is3D() && isMeasure() && coords.size() >= 4 )
+ {
+ // NURBSCURVE ZM pattern: (x y z m)
+ double z = coords[2].toDouble( &ok );
+ if ( !ok )
+ return false;
+ double m = coords[3].toDouble( &ok );
+ if ( !ok )
+ return false;
+ point = QgsPoint( x, y, z, m );
+ }
+ else if ( isMeasure() && coords.size() >= 4 )
+ {
+ // NURBSCURVE M pattern with 4 coords: (x y z m) - upgrade to ZM
+ double z = coords[2].toDouble( &ok );
+ if ( !ok )
+ return false;
+ double m = coords[3].toDouble( &ok );
+ if ( !ok )
+ return false;
+ point = QgsPoint( x, y, z, m );
+ if ( !is3D() )
+ mWkbType = QgsWkbTypes::addZ( mWkbType );
+ }
+ else if ( !is3D() && !isMeasure() && coords.size() == 3 )
+ {
+ // No explicit dimension - auto-upgrade to 3D: (x y z)
+ double z = coords[2].toDouble( &ok );
+ if ( !ok )
+ return false;
+ point = QgsPoint( x, y, z );
+ mWkbType = QgsWkbTypes::addZ( mWkbType );
+ }
+ else if ( !is3D() && !isMeasure() && coords.size() >= 4 )
+ {
+ // No explicit dimension - auto-upgrade to ZM: (x y z m)
+ double z = coords[2].toDouble( &ok );
+ if ( !ok )
+ return false;
+ double m = coords[3].toDouble( &ok );
+ if ( !ok )
+ return false;
+ point = QgsPoint( x, y, z, m );
+ mWkbType = QgsWkbTypes::addZ( mWkbType );
+ mWkbType = QgsWkbTypes::addM( mWkbType );
+ }
+ else
+ {
+ point = QgsPoint( x, y );
+ }
+ }
+ else
+ {
+ point = QgsPoint( x, y );
+ }
+
+ controlPoints.append( point );
+ }
+
+ mControlPoints = controlPoints;
+
+ // Initialize weights to 1.0 (non-rational by default)
+ mWeights.clear();
+ for ( int i = 0; i < controlPoints.size(); ++i )
+ {
+ mWeights.append( 1.0 );
+ }
+
+ // Parse additional parameters (degree already parsed at the beginning)
+ bool hasWeights = false;
+ bool hasKnots = false;
+
+ // Process remaining blocks (starting from index 2 since 0=degree, 1=control points)
+ for ( int i = 2; i < blocks.size(); ++i )
+ {
+ QString block = blocks[i].trimmed();
+
+ if ( block.startsWith( QLatin1Char( '(' ) ) )
+ {
+ // Validate block ends with ')'
+ if ( !block.endsWith( QLatin1Char( ')' ) ) )
+ return false;
+
+ // This could be weights or knots vector
+ block = block.mid( 1, block.length() - 2 ).trimmed();
+ QStringList values = block.split( ',', Qt::SkipEmptyParts );
+
+ QVector parsedValues;
+ for ( const QString &valueStr : values )
+ {
+ bool ok = true;
+ double value = valueStr.trimmed().toDouble( &ok );
+ if ( !ok )
+ return false;
+ parsedValues.append( value );
+ }
+
+ if ( !hasWeights && parsedValues.size() == controlPoints.size() )
+ {
+ // This is the weights vector
+ mWeights = parsedValues;
+ hasWeights = true;
+ }
+ else if ( !hasKnots )
+ {
+ // This is the knots vector
+ mKnots = parsedValues;
+ hasKnots = true;
+ }
+ }
+ else
+ {
+ // Invalid block - doesn't start with '('
+ return false;
+ }
+ }
+
+ mDegree = degree;
+
+ // If no knots were provided, create default knots (open uniform)
+ if ( !hasKnots )
+ {
+ const int n = controlPoints.size();
+ const int knotsSize = n + degree + 1;
+ mKnots.clear();
+ mKnots.reserve( knotsSize );
+
+ // Open uniform knot vector
+ for ( int i = 0; i < knotsSize; ++i )
+ {
+ if ( i <= degree )
+ mKnots.append( 0.0 );
+ else if ( i >= n )
+ mKnots.append( 1.0 );
+ else
+ mKnots.append( static_cast( i - degree ) / ( n - degree ) );
+ }
+ }
+
+ return true;
+}
+
+bool QgsNurbsCurve::fuzzyEqual( const QgsAbstractGeometry &other, double epsilon ) const
+{
+ const QgsNurbsCurve *o = qgsgeometry_cast( &other );
+ if ( !o )
+ return false;
+
+ if ( mDegree != o->mDegree || mControlPoints.size() != o->mControlPoints.size() || mWeights.size() != o->mWeights.size() || mKnots.size() != o->mKnots.size() )
+ {
+ return false;
+ }
+
+ for ( int i = 0; i < mControlPoints.size(); ++i )
+ {
+ if ( mControlPoints[i].distance( o->mControlPoints[i] ) >= epsilon )
+ return false;
+ }
+
+ for ( int i = 0; i < mWeights.size(); ++i )
+ {
+ if ( std::fabs( mWeights[i] - o->mWeights[i] ) > epsilon )
+ return false;
+ }
+
+ for ( int i = 0; i < mKnots.size(); ++i )
+ {
+ if ( std::fabs( mKnots[i] - o->mKnots[i] ) > epsilon )
+ return false;
+ }
+
+ return true;
+}
+
+bool QgsNurbsCurve::fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon ) const
+{
+ return fuzzyEqual( other, epsilon );
+}
+
+QString QgsNurbsCurve::geometryType() const
+{
+ return QStringLiteral( "NurbsCurve" );
+}
+
+bool QgsNurbsCurve::hasCurvedSegments() const
+{
+ return true;
+}
+
+int QgsNurbsCurve::partCount() const
+{
+ return 1;
+}
+
+QgsCurve *QgsNurbsCurve::toCurveType() const
+{
+ return new QgsNurbsCurve( *this );
+}
+
+QgsPoint QgsNurbsCurve::vertexAt( QgsVertexId id ) const
+{
+ if ( id.part != 0 || id.ring != 0 )
+ {
+ return QgsPoint();
+ }
+ const int idx = id.vertex;
+ if ( idx < 0 || idx >= mControlPoints.size() )
+ {
+ return QgsPoint();
+ }
+ return mControlPoints[idx];
+}
+
+int QgsNurbsCurve::vertexCount( int part, int ring ) const
+{
+ return ( part == 0 && ring == 0 ) ? mControlPoints.size() : 0;
+}
+
+int QgsNurbsCurve::vertexNumberFromVertexId( QgsVertexId id ) const
+{
+ if ( id.part == 0 && id.ring == 0 )
+ {
+ return id.vertex;
+ }
+ return -1;
+}
+
+bool QgsNurbsCurve::isValid( QString &error, Qgis::GeometryValidityFlags flags ) const
+{
+ Q_UNUSED( flags );
+
+ // Use cached validity if available
+ if ( mValidityComputed )
+ {
+ if ( !mIsValid )
+ error = QStringLiteral( "NURBS curve is invalid" );
+ return mIsValid;
+ }
+
+ mValidityComputed = true;
+ mIsValid = false;
+
+ if ( mDegree < 1 )
+ {
+ error = QStringLiteral( "Degree must be >= 1" );
+ return false;
+ }
+
+ const int n = mControlPoints.size();
+ if ( n < mDegree + 1 )
+ {
+ error = QStringLiteral( "Not enough control points for degree" );
+ return false;
+ }
+
+ if ( mKnots.size() != n + mDegree + 1 )
+ {
+ error = QStringLiteral( "Knot vector size is incorrect" );
+ return false;
+ }
+
+ if ( mWeights.size() != n )
+ {
+ error = QStringLiteral( "Weights vector size mismatch" );
+ return false;
+ }
+
+ // Check that knots are non-decreasing
+ for ( int i = 1; i < mKnots.size(); ++i )
+ {
+ if ( mKnots[i] < mKnots[i - 1] )
+ {
+ error = QStringLiteral( "Knot vector values must be non-decreasing" );
+ return false;
+ }
+ }
+
+ mIsValid = true;
+ return true;
+}
+
+void QgsNurbsCurve::addToPainterPath( QPainterPath &path ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( line )
+ line->addToPainterPath( path );
+}
+
+QgsCurve *QgsNurbsCurve::curveSubstring( double startDistance, double endDistance ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ return nullptr;
+ return line->curveSubstring( startDistance, endDistance );
+}
+
+double QgsNurbsCurve::length() const
+{
+ std::unique_ptr line( curveToLine() );
+ return line ? line->length() : 0.0;
+}
+
+double QgsNurbsCurve::segmentLength( QgsVertexId startVertex ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ return 0.0;
+ return line->segmentLength( startVertex );
+}
+
+double QgsNurbsCurve::distanceBetweenVertices( QgsVertexId fromVertex, QgsVertexId toVertex ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ return -1.0;
+ return line->distanceBetweenVertices( fromVertex, toVertex );
+}
+
+QgsAbstractGeometry *QgsNurbsCurve::snappedToGrid( double hSpacing, double vSpacing, double dSpacing, double mSpacing, bool removeRedundantPoints ) const
+{
+ auto result = new QgsNurbsCurve( *this );
+ for ( QgsPoint &pt : result->mControlPoints )
+ {
+ if ( hSpacing > 0 )
+ pt.setX( std::round( pt.x() / hSpacing ) * hSpacing );
+ if ( vSpacing > 0 )
+ pt.setY( std::round( pt.y() / vSpacing ) * vSpacing );
+ if ( pt.is3D() && dSpacing > 0 )
+ pt.setZ( std::round( pt.z() / dSpacing ) * dSpacing );
+ if ( pt.isMeasure() && mSpacing > 0 )
+ pt.setM( std::round( pt.m() / mSpacing ) * mSpacing );
+ }
+
+ if ( removeRedundantPoints )
+ result->removeDuplicateNodes();
+
+ return result;
+}
+
+QgsAbstractGeometry *QgsNurbsCurve::simplifyByDistance( double tolerance ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ return new QgsNurbsCurve( *this );
+ return line->simplifyByDistance( tolerance );
+}
+
+bool QgsNurbsCurve::removeDuplicateNodes( double epsilon, bool useZValues )
+{
+ if ( mControlPoints.size() < 2 )
+ return false;
+
+ QVector newPoints;
+ QVector newWeights;
+
+ newPoints.reserve( mControlPoints.size() );
+ newWeights.reserve( mWeights.size() );
+
+ newPoints.append( mControlPoints.first() );
+ if ( !mWeights.isEmpty() )
+ newWeights.append( mWeights.first() );
+
+ for ( int i = 1; i < mControlPoints.size(); ++i )
+ {
+ double dist;
+ if ( useZValues && mControlPoints[i].is3D() && mControlPoints[i - 1].is3D() )
+ {
+ dist = mControlPoints[i].distance3D( mControlPoints[i - 1] );
+ }
+ else
+ {
+ dist = mControlPoints[i].distance( mControlPoints[i - 1] );
+ }
+
+ if ( dist >= epsilon )
+ {
+ newPoints.append( mControlPoints[i] );
+ if ( i < mWeights.size() )
+ newWeights.append( mWeights[i] );
+ }
+ }
+
+ const bool changed = ( newPoints.size() != mControlPoints.size() );
+ if ( !changed )
+ return false;
+
+ mControlPoints = newPoints;
+ mWeights = newWeights;
+
+ // Regenerate uniform knot vector for the new number of control points
+ const int n = mControlPoints.size();
+ const int knotsSize = n + mDegree + 1;
+ mKnots.clear();
+ mKnots.reserve( knotsSize );
+ for ( int i = 0; i < knotsSize; ++i )
+ {
+ if ( i <= mDegree )
+ mKnots.append( 0.0 );
+ else if ( i >= n )
+ mKnots.append( 1.0 );
+ else
+ mKnots.append( static_cast( i - mDegree ) / ( n - mDegree ) );
+ }
+
+ clearCache();
+ return true;
+}
+
+double QgsNurbsCurve::vertexAngle( QgsVertexId vertex ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ return 0.0;
+ return line->vertexAngle( vertex );
+}
+
+void QgsNurbsCurve::swapXy()
+{
+ for ( QgsPoint &pt : mControlPoints )
+ {
+ const double x = pt.x();
+ pt.setX( pt.y() );
+ pt.setY( x );
+ }
+ clearCache();
+}
+
+bool QgsNurbsCurve::transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback )
+{
+ Q_UNUSED( feedback );
+ if ( !transformer )
+ return false;
+
+ for ( QgsPoint &pt : mControlPoints )
+ {
+ double x = pt.x(), y = pt.y(), z = pt.z(), m = pt.m();
+ if ( !transformer->transformPoint( x, y, z, m ) )
+ return false;
+ pt.setX( x );
+ pt.setY( y );
+ pt.setZ( z );
+ pt.setM( m );
+ }
+
+ clearCache();
+ return true;
+}
+
+QgsAbstractGeometry *QgsNurbsCurve::createEmptyWithSameType() const
+{
+ return new QgsNurbsCurve();
+}
+
+double QgsNurbsCurve::closestSegment( const QgsPoint &pt, QgsPoint &segmentPt, QgsVertexId &vertexAfter, int *leftOf, double epsilon ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ {
+ segmentPt = QgsPoint();
+ vertexAfter = QgsVertexId();
+ if ( leftOf )
+ *leftOf = 0;
+ return -1;
+ }
+ return line->closestSegment( pt, segmentPt, vertexAfter, leftOf, epsilon );
+}
+
+void QgsNurbsCurve::transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d, bool transformZ )
+{
+ for ( QgsPoint &pt : mControlPoints )
+ {
+ double x = pt.x();
+ double y = pt.y();
+ double z = transformZ && pt.is3D() ? pt.z() : std::numeric_limits::quiet_NaN();
+ ct.transformInPlace( x, y, z, d );
+ pt.setX( x );
+ pt.setY( y );
+ if ( transformZ && pt.is3D() )
+ pt.setZ( z );
+ }
+ clearCache();
+}
+
+void QgsNurbsCurve::transform( const QTransform &t, double zTranslate, double zScale, double mTranslate, double mScale )
+{
+ for ( QgsPoint &pt : mControlPoints )
+ {
+ const QPointF p = t.map( QPointF( pt.x(), pt.y() ) );
+ pt.setX( p.x() );
+ pt.setY( p.y() );
+
+ if ( pt.is3D() )
+ pt.setZ( pt.z() * zScale + zTranslate );
+ if ( pt.isMeasure() )
+ pt.setM( pt.m() * mScale + mTranslate );
+ }
+ clearCache();
+}
+
+QgsRectangle QgsNurbsCurve::boundingBox() const
+{
+ return boundingBox3D().toRectangle();
+}
+
+QgsBox3D QgsNurbsCurve::boundingBox3D() const
+{
+ if ( mBoundingBox.isNull() )
+ {
+ mBoundingBox = calculateBoundingBox3D();
+ }
+ return mBoundingBox;
+}
+
+QgsBox3D QgsNurbsCurve::calculateBoundingBox3D() const
+{
+ if ( mControlPoints.isEmpty() )
+ return QgsBox3D();
+
+ // The bounding box must include all control points, not just points on the curve.
+ // This is important for Snapping to control points (they can lie outside the curve itself)
+ QgsBox3D bbox;
+ for ( const QgsPoint &pt : mControlPoints )
+ {
+ bbox.combineWith( pt.x(), pt.y(), pt.is3D() ? pt.z() : std::numeric_limits::quiet_NaN() );
+ }
+
+ // Also include points on the curve to ensure the bbox is complete
+ std::unique_ptr line( curveToLine() );
+ if ( line )
+ {
+ bbox.combineWith( line->boundingBox3D() );
+ }
+
+ return bbox;
+}
+
+void QgsNurbsCurve::clearCache() const
+{
+ QgsCurve::clearCache();
+ mValidityComputed = false;
+ mIsValid = false;
+}
+
+bool QgsNurbsCurve::moveVertex( QgsVertexId position, const QgsPoint &newPos )
+{
+ if ( position.part != 0 || position.ring != 0 )
+ return false;
+
+ const int idx = position.vertex;
+ if ( idx < 0 || idx >= mControlPoints.size() )
+ return false;
+
+ mControlPoints[idx] = newPos;
+ clearCache();
+ return true;
+}
+
+bool QgsNurbsCurve::insertVertex( QgsVertexId position, const QgsPoint &vertex )
+{
+ if ( position.part != 0 || position.ring != 0 )
+ return false;
+
+ const int idx = position.vertex;
+ if ( idx < 0 || idx > mControlPoints.size() )
+ return false;
+
+ mControlPoints.insert( idx, vertex );
+ if ( idx <= mWeights.size() )
+ mWeights.insert( idx, 1.0 );
+
+ const int n = mControlPoints.size();
+ const int knotsSize = n + mDegree + 1;
+ mKnots.clear();
+ mKnots.reserve( knotsSize );
+ for ( int i = 0; i < knotsSize; ++i )
+ {
+ if ( i <= mDegree )
+ mKnots.append( 0.0 );
+ else if ( i >= n )
+ mKnots.append( 1.0 );
+ else
+ mKnots.append( static_cast( i - mDegree ) / ( n - mDegree ) );
+ }
+
+ clearCache();
+ return true;
+}
+
+int QgsNurbsCurve::wkbSize( QgsAbstractGeometry::WkbFlags flags ) const
+{
+ Q_UNUSED( flags );
+
+ const bool is3D = QgsWkbTypes::hasZ( mWkbType );
+ const bool isMeasure = QgsWkbTypes::hasM( mWkbType );
+ const int coordinateDimension = 2 + ( is3D ? 1 : 0 ) + ( isMeasure ? 1 : 0 );
+
+ int size = 0;
+
+ // WKB header (endianness + type)
+ size += 1 + 4;
+
+ // Degree (4 bytes)
+ size += 4;
+
+ // Number of control points (4 bytes)
+ size += 4;
+
+ // Control points data
+ for ( int i = 0; i < mControlPoints.size(); ++i )
+ {
+ // Point byte order (1 byte)
+ size += 1;
+
+ // Coordinates (8 bytes per coordinate)
+ size += coordinateDimension * 8;
+
+ // Weight flag (1 byte)
+ size += 1;
+
+ // Weight value if not default (8 bytes)
+ if ( i < mWeights.size() && std::fabs( mWeights[i] - 1.0 ) > 1e-10 )
+ size += 8;
+ }
+
+ // Number of knots (4 bytes)
+ size += 4;
+
+ // Knot values (8 bytes each)
+ size += mKnots.size() * 8;
+
+ return size;
+}
+
+QByteArray QgsNurbsCurve::asWkb( QgsAbstractGeometry::WkbFlags flags ) const
+{
+ QByteArray wkbArray;
+ wkbArray.resize( QgsNurbsCurve::wkbSize( flags ) );
+ QgsWkbPtr wkbPtr( wkbArray );
+
+ // Write WKB header
+ wkbPtr << static_cast( QgsApplication::endian() );
+ wkbPtr << static_cast( mWkbType );
+
+ // Write degree (4 bytes uint32)
+ wkbPtr << static_cast( mDegree );
+
+ // Write number of control points (4 bytes uint32)
+ wkbPtr << static_cast( mControlPoints.size() );
+
+ const bool is3D = QgsWkbTypes::hasZ( mWkbType );
+ const bool isMeasure = QgsWkbTypes::hasM( mWkbType );
+
+ // Write control points
+ for ( int i = 0; i < mControlPoints.size(); ++i )
+ {
+ const QgsPoint &point = mControlPoints[i];
+
+ // Write byte order for this point (1 byte) - use same as global
+ wkbPtr << static_cast( QgsApplication::endian() );
+
+ // Write coordinates
+ wkbPtr << point.x() << point.y();
+
+ if ( is3D )
+ wkbPtr << point.z();
+ if ( isMeasure )
+ wkbPtr << point.m();
+
+ // Write weight flag and weight
+ const double weight = ( i < mWeights.size() ) ? mWeights[i] : 1.0;
+ const bool hasCustomWeight = std::fabs( weight - 1.0 ) > 1e-10;
+
+ wkbPtr << static_cast( hasCustomWeight ? 1 : 0 );
+
+ if ( hasCustomWeight )
+ {
+ wkbPtr << weight;
+ }
+ }
+
+ // Write number of knots (4 bytes uint32)
+ wkbPtr << static_cast( mKnots.size() );
+
+ // Write knot values (8 bytes double each)
+ for ( const double knot : mKnots )
+ {
+ wkbPtr << knot;
+ }
+
+ return wkbArray;
+}
+
+QString QgsNurbsCurve::asWkt( int precision ) const
+{
+ QString wkt = wktTypeStr();
+
+ if ( isEmpty() )
+ {
+ wkt += QLatin1String( " EMPTY" );
+ }
+ else
+ {
+ wkt += QLatin1String( " (" );
+
+ // Add degree first
+ wkt += QString::number( mDegree );
+
+ // Add control points
+ wkt += QLatin1String( ", (" );
+ for ( int i = 0; i < mControlPoints.size(); ++i )
+ {
+ if ( i > 0 )
+ wkt += QLatin1String( ", " );
+
+ const QgsPoint &pt = mControlPoints[i];
+ wkt += qgsDoubleToString( pt.x(), precision ) + ' ' + qgsDoubleToString( pt.y(), precision );
+
+ if ( pt.is3D() )
+ wkt += ' ' + qgsDoubleToString( pt.z(), precision );
+
+ if ( pt.isMeasure() )
+ wkt += ' ' + qgsDoubleToString( pt.m(), precision );
+ }
+ wkt += ')';
+
+ // Always add weights if they exist to ensure round-trip consistency
+ if ( !mWeights.isEmpty() )
+ {
+ wkt += QLatin1String( ", (" );
+ for ( int i = 0; i < mWeights.size(); ++i )
+ {
+ if ( i > 0 )
+ wkt += QLatin1String( ", " );
+ wkt += qgsDoubleToString( mWeights[i], precision );
+ }
+ wkt += ')';
+ }
+
+ // Always add knots if they exist to ensure round-trip consistency
+ if ( !mKnots.isEmpty() )
+ {
+ wkt += QLatin1String( ", (" );
+ for ( int i = 0; i < mKnots.size(); ++i )
+ {
+ if ( i > 0 )
+ wkt += QLatin1String( ", " );
+ wkt += qgsDoubleToString( mKnots[i], precision );
+ }
+ wkt += ')';
+ }
+
+ wkt += ')';
+ }
+
+ return wkt;
+}
+
+QDomElement QgsNurbsCurve::asGml2( QDomDocument &doc, int precision, const QString &ns, QgsAbstractGeometry::AxisOrder axisOrder ) const
+{
+ // GML2 does not support NURBS curves, convert to LineString
+ // TODO: GML3 has BSpline support, but it's not clear how it's handled elsewhere in QGIS
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ return QDomElement();
+ return line->asGml2( doc, precision, ns, axisOrder );
+}
+
+QDomElement QgsNurbsCurve::asGml3( QDomDocument &doc, int precision, const QString &ns, QgsAbstractGeometry::AxisOrder axisOrder ) const
+{
+ // TODO: GML3 has native BSpline support (gml:BSpline), but it's not clear how it's handled elsewhere in QGIS
+ // For now, convert to LineString for compatibility
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ return QDomElement();
+ return line->asGml3( doc, precision, ns, axisOrder );
+}
+
+json QgsNurbsCurve::asJsonObject( int precision ) const
+{
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ return json::object();
+ return line->asJsonObject( precision );
+}
+
+QString QgsNurbsCurve::asKml( int precision ) const
+{
+ // KML does not support NURBS curves, convert to LineString
+ std::unique_ptr line( curveToLine() );
+ if ( !line )
+ return QString();
+ return line->asKml( precision );
+}
+
+int QgsNurbsCurve::dimension() const
+{
+ return 1;
+}
+
+bool QgsNurbsCurve::isEmpty() const
+{
+ return mControlPoints.isEmpty();
+}
+
+void QgsNurbsCurve::clear()
+{
+ mControlPoints.clear();
+ mKnots.clear();
+ mWeights.clear();
+ mDegree = 0;
+ clearCache();
+}
+
+bool QgsNurbsCurve::boundingBoxIntersects( const QgsRectangle &rectangle ) const
+{
+ return boundingBox().intersects( rectangle );
+}
+
+bool QgsNurbsCurve::boundingBoxIntersects( const QgsBox3D &box3d ) const
+{
+ return boundingBox3D().intersects( box3d );
+}
+
+QgsPoint QgsNurbsCurve::centroid() const
+{
+ std::unique_ptr line( curveToLine() );
+ return line ? line->centroid() : QgsPoint();
+}
+
+int QgsNurbsCurve::compareToSameClass( const QgsAbstractGeometry *other ) const
+{
+ const QgsNurbsCurve *otherCurve = qgsgeometry_cast( other );
+ if ( !otherCurve )
+ return -1;
+
+ if ( mDegree < otherCurve->mDegree )
+ return -1;
+ else if ( mDegree > otherCurve->mDegree )
+ return 1;
+
+ const int nThis = mControlPoints.size();
+ const int nOther = otherCurve->mControlPoints.size();
+
+ if ( nThis < nOther )
+ return -1;
+ else if ( nThis > nOther )
+ return 1;
+
+ for ( int i = 0; i < nThis; ++i )
+ {
+ if ( mControlPoints[i].x() < otherCurve->mControlPoints[i].x() )
+ return -1;
+ else if ( mControlPoints[i].x() > otherCurve->mControlPoints[i].x() )
+ return 1;
+ else if ( mControlPoints[i].y() < otherCurve->mControlPoints[i].y() )
+ return -1;
+ else if ( mControlPoints[i].y() > otherCurve->mControlPoints[i].y() )
+ return 1;
+ }
+
+ if ( mWeights.size() < otherCurve->mWeights.size() )
+ return -1;
+ else if ( mWeights.size() > otherCurve->mWeights.size() )
+ return 1;
+
+ for ( int i = 0; i < mWeights.size(); ++i )
+ {
+ if ( mWeights[i] < otherCurve->mWeights[i] )
+ return -1;
+ else if ( mWeights[i] > otherCurve->mWeights[i] )
+ return 1;
+ }
+
+ if ( mKnots.size() < otherCurve->mKnots.size() )
+ return -1;
+ else if ( mKnots.size() > otherCurve->mKnots.size() )
+ return 1;
+
+ for ( int i = 0; i < mKnots.size(); ++i )
+ {
+ if ( mKnots[i] < otherCurve->mKnots[i] )
+ return -1;
+ else if ( mKnots[i] > otherCurve->mKnots[i] )
+ return 1;
+ }
+
+ return 0;
+}
+
+double QgsNurbsCurve::weight( int index ) const
+{
+ if ( index < 0 || index >= mWeights.size() )
+ return 1.0;
+ return mWeights[index];
+}
+
+bool QgsNurbsCurve::setWeight( int index, double weight )
+{
+ if ( index < 0 || index >= mWeights.size() )
+ return false;
+ if ( weight <= 0.0 )
+ return false;
+ mWeights[index] = weight;
+ clearCache();
+ return true;
+}
diff --git a/src/core/geometry/qgsnurbscurve.h b/src/core/geometry/qgsnurbscurve.h
new file mode 100644
index 000000000000..59fd78d8a3dd
--- /dev/null
+++ b/src/core/geometry/qgsnurbscurve.h
@@ -0,0 +1,285 @@
+/***************************************************************************
+ qgsnurbscurve.h
+ -----------------
+ begin : September 2025
+ copyright : (C) 2025 by Loïc Bartoletti
+ email : loic dot bartoletti at oslandia dot com
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef QGSNURBSCURVE_H
+#define QGSNURBSCURVE_H
+
+#include
+#include
+
+#include "qgis_core.h"
+#include "qgis_sip.h"
+#include "qgscurve.h"
+
+#include
+
+/**
+ * \ingroup core
+ * \class QgsNurbsCurve
+ * \brief Represents a NURBS (Non-Uniform Rational B-Spline) curve geometry in 2D/3D.
+ *
+ * NURBS curves are a mathematical model commonly used in computer graphics
+ * for representing curves. They are parametric curves defined by control points,
+ * weights, knot vectors, and a degree.
+ *
+ * \since QGIS 4.0
+ */
+class CORE_EXPORT QgsNurbsCurve : public QgsCurve
+{
+ public:
+ /**
+ * Constructor for an empty NURBS curve geometry.
+ */
+ QgsNurbsCurve();
+
+ /**
+ * Constructs a NURBS curve from control points, degree, knot vector and weights.
+ * \param ctrlPoints control points defining the curve. The number of control points must be strictly greater than \a degree
+ * \param degree degree of the NURBS curve (must be >= 1, typically 1-3)
+ * \param knots knot vector (must have size = control points count + degree + 1, values must be non-decreasing)
+ * \param weights weight vector for rational curves (same size as control points)
+ */
+ QgsNurbsCurve( const QVector &ctrlPoints, int degree, const QVector &knots, const QVector &weights );
+
+ QgsCurve *clone() const override SIP_FACTORY;
+
+ /**
+ * Evaluates the NURBS curve at parameter t ∈ [0,1].
+ * Uses the Cox-de Boor algorithm for B-spline basis function evaluation.
+ * \param t parameter value between 0 and 1
+ * \returns point on the curve at parameter t
+ */
+ [[nodiscard]] QgsPoint evaluate( double t ) const;
+
+ /**
+ * Returns TRUE if this curve represents a Bézier curve.
+ * A Bézier curve is a special case of NURBS with uniform weights and specific knot vector.
+ */
+ bool isBezier() const;
+
+ /**
+ * Returns TRUE if this curve represents a B-spline (non-rational NURBS).
+ */
+ bool isBSpline() const;
+
+ /**
+ * Returns TRUE if this curve is rational (has non-uniform weights).
+ */
+ bool isRational() const;
+
+ /**
+ * Returns TRUE if this curve represents a poly-Bézier curve.
+ * A poly-Bézier is a degree 3 NURBS with (n-1) divisible by 3,
+ * where n is the number of control points.
+ */
+ bool isPolyBezier() const;
+
+ bool isClosed() const override SIP_HOLDGIL;
+ bool isClosed2D() const override SIP_HOLDGIL;
+
+ // QgsCurve interface
+ QgsLineString *curveToLine( double tolerance = M_PI_2 / 90, SegmentationToleranceType toleranceType = MaximumAngle ) const override SIP_FACTORY;
+ void draw( QPainter &p ) const override;
+ void drawAsPolygon( QPainter &p ) const override;
+ QgsPoint endPoint() const override SIP_HOLDGIL;
+ bool equals( const QgsCurve &other ) const override;
+ int indexOf( const QgsPoint &point ) const override;
+ QgsPoint *interpolatePoint( double distance ) const override SIP_FACTORY;
+ int numPoints() const override SIP_HOLDGIL;
+ bool pointAt( int node, QgsPoint &point SIP_OUT, Qgis::VertexType &type SIP_OUT ) const override;
+ void points( QgsPointSequence &pts SIP_OUT ) const override;
+ QgsCurve *reversed() const override SIP_FACTORY;
+ void scroll( int firstVertexIndex ) override;
+ std::tuple, std::unique_ptr> splitCurveAtVertex( int index ) const override SIP_SKIP;
+ QgsPoint startPoint() const override SIP_HOLDGIL;
+ void sumUpArea( double &sum SIP_OUT ) const override;
+ double xAt( int index ) const override;
+ double yAt( int index ) const override;
+ double zAt( int index ) const override;
+ double mAt( int index ) const override;
+
+ QPolygonF asQPolygonF() const override;
+
+ void addToPainterPath( QPainterPath &path ) const override;
+ QgsCurve *curveSubstring( double startDistance, double endDistance ) const override SIP_FACTORY;
+ double length() const override SIP_HOLDGIL;
+ double segmentLength( QgsVertexId startVertex ) const override;
+ double distanceBetweenVertices( QgsVertexId fromVertex, QgsVertexId toVertex ) const override;
+ QgsAbstractGeometry *snappedToGrid( double hSpacing, double vSpacing, double dSpacing = 0, double mSpacing = 0, bool removeRedundantPoints = false ) const override SIP_FACTORY;
+ QgsAbstractGeometry *simplifyByDistance( double tolerance ) const override SIP_FACTORY;
+ bool removeDuplicateNodes( double epsilon = 4 * std::numeric_limits::epsilon(), bool useZValues = false ) override;
+ double vertexAngle( QgsVertexId vertex ) const override;
+ void swapXy() override;
+ bool transform( QgsAbstractGeometryTransformer *transformer, QgsFeedback *feedback = nullptr ) override;
+ QgsAbstractGeometry *createEmptyWithSameType() const override SIP_FACTORY;
+ double closestSegment( const QgsPoint &pt, QgsPoint &segmentPt SIP_OUT, QgsVertexId &vertexAfter SIP_OUT, int *leftOf SIP_OUT = nullptr, double epsilon = 4 * std::numeric_limits::epsilon() ) const override;
+ void transform( const QgsCoordinateTransform &ct, Qgis::TransformDirection d = Qgis::TransformDirection::Forward, bool transformZ = false ) override SIP_THROW( QgsCsException );
+ void transform( const QTransform &t, double zTranslate = 0.0, double zScale = 1.0, double mTranslate = 0.0, double mScale = 1.0 ) override;
+ QgsRectangle boundingBox() const override;
+ QgsBox3D boundingBox3D() const override;
+ bool moveVertex( QgsVertexId position, const QgsPoint &newPos ) override;
+ bool insertVertex( QgsVertexId position, const QgsPoint &vertex ) override;
+ int wkbSize( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const override;
+ QByteArray asWkb( QgsAbstractGeometry::WkbFlags flags = QgsAbstractGeometry::WkbFlags() ) const override;
+ QString asWkt( int precision = 17 ) const override;
+ QDomElement asGml2( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const override;
+ QDomElement asGml3( QDomDocument &doc, int precision = 17, const QString &ns = "gml", QgsAbstractGeometry::AxisOrder axisOrder = QgsAbstractGeometry::AxisOrder::XY ) const override;
+ json asJsonObject( int precision = 17 ) const override SIP_SKIP;
+ QString asKml( int precision = 17 ) const override;
+ int dimension() const override SIP_HOLDGIL;
+ bool isEmpty() const override SIP_HOLDGIL;
+ void clear() override;
+ bool boundingBoxIntersects( const QgsRectangle &rectangle ) const override SIP_HOLDGIL;
+ bool boundingBoxIntersects( const QgsBox3D &box3d ) const override SIP_HOLDGIL;
+ QgsPoint centroid() const override;
+
+ // QgsAbstractGeometry interface
+ bool addZValue( double zValue = 0 ) override;
+ bool addMValue( double mValue = 0 ) override;
+ bool dropZValue() override;
+ bool dropMValue() override;
+ bool deleteVertex( QgsVertexId position ) override;
+#ifndef SIP_RUN
+ void filterVertices( const std::function &filter ) override;
+#endif
+ bool fromWkb( QgsConstWkbPtr &wkb ) override;
+ bool fromWkt( const QString &wkt ) override;
+ bool fuzzyEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const override SIP_HOLDGIL;
+ bool fuzzyDistanceEqual( const QgsAbstractGeometry &other, double epsilon = 1e-8 ) const override SIP_HOLDGIL;
+ QString geometryType() const override SIP_HOLDGIL;
+ bool hasCurvedSegments() const override SIP_HOLDGIL;
+ int partCount() const override SIP_HOLDGIL;
+ QgsCurve *toCurveType() const override;
+ QgsPoint vertexAt( QgsVertexId id ) const override;
+ int vertexCount( int part = 0, int ring = 0 ) const override SIP_HOLDGIL;
+ int vertexNumberFromVertexId( QgsVertexId id ) const override;
+ bool isValid( QString &error SIP_OUT, Qgis::GeometryValidityFlags flags = Qgis::GeometryValidityFlags() ) const override;
+
+ /**
+ * Returns the degree of the NURBS curve.
+ */
+ int degree() const SIP_HOLDGIL { return mDegree; }
+
+ /**
+ * Sets the degree of the NURBS curve.
+ * \param degree curve degree (typically 1-3)
+ */
+ void setDegree( int degree )
+ {
+ mDegree = degree;
+ clearCache();
+ }
+
+ /**
+ * Returns the control points of the NURBS curve.
+ */
+ const QVector &controlPoints() const SIP_HOLDGIL { return mControlPoints; }
+
+ /**
+ * Sets the control points of the NURBS curve.
+ * \param points control points
+ */
+ void setControlPoints( const QVector &points )
+ {
+ mControlPoints = points;
+ clearCache();
+ }
+
+ /**
+ * Returns the knot vector of the NURBS curve.
+ */
+ const QVector &knots() const SIP_HOLDGIL { return mKnots; }
+
+ /**
+ * Sets the knot vector of the NURBS curve.
+ * \param knots knot vector (must have size = control points count + degree + 1, values must be non-decreasing)
+ */
+ void setKnots( const QVector &knots )
+ {
+ mKnots = knots;
+ clearCache();
+ }
+
+ /**
+ * Returns the weight vector of the NURBS curve.
+ */
+ const QVector &weights() const SIP_HOLDGIL { return mWeights; }
+
+ /**
+ * Sets the weight vector of the NURBS curve.
+ * \param weights weight vector (same size as control points)
+ */
+ void setWeights( const QVector &weights )
+ {
+ mWeights = weights;
+ clearCache();
+ }
+
+ /**
+ * Returns the weight at the specified control point \a index.
+ * Returns 1.0 if index is out of range.
+ * \since QGIS 4.0
+ */
+ double weight( int index ) const SIP_HOLDGIL;
+
+ /**
+ * Sets the \a weight at the specified control point \a index.
+ * Weight must be positive (> 0).
+ * \returns TRUE if successful, FALSE if index is out of range or weight is invalid.
+ * \since QGIS 4.0
+ */
+ bool setWeight( int index, double weight );
+
+ /**
+ * Cast the \a geom to a QgsNurbsCurve.
+ * Should be used by qgsgeometry_cast( geometry ).
+ * \note Not available in Python.
+ */
+ inline static const QgsNurbsCurve *cast( const QgsAbstractGeometry *geom ) SIP_SKIP // cppcheck-suppress duplInheritedMember
+ {
+ if ( geom && geom->geometryType() == QLatin1String( "NurbsCurve" ) )
+ return static_cast( geom );
+ return nullptr;
+ }
+
+ /**
+ * Cast the \a geom to a QgsNurbsCurve.
+ * Should be used by qgsgeometry_cast( geometry ).
+ * \note Not available in Python.
+ */
+ inline static QgsNurbsCurve *cast( QgsAbstractGeometry *geom ) SIP_SKIP // cppcheck-suppress duplInheritedMember
+ {
+ if ( geom && geom->geometryType() == QLatin1String( "NurbsCurve" ) )
+ return static_cast( geom );
+ return nullptr;
+ }
+
+ protected:
+ void clearCache() const override;
+ int compareToSameClass( const QgsAbstractGeometry *other ) const final;
+ QgsBox3D calculateBoundingBox3D() const override;
+
+ private:
+ QVector mControlPoints; //! Control points defining the curve shape
+ QVector mKnots; //! Knot vector for B-spline basis functions
+ QVector mWeights; //! Weight vector for rational curves
+ int mDegree = 0; //! Degree of the NURBS curve
+ mutable bool mValidityComputed = false; //! Whether validity has been computed
+ mutable bool mIsValid = false; //! Cached validity state
+};
+
+#endif // QGSNURBSCURVE_H
diff --git a/src/core/geometry/qgsnurbsutils.cpp b/src/core/geometry/qgsnurbsutils.cpp
new file mode 100644
index 000000000000..0410a42b750e
--- /dev/null
+++ b/src/core/geometry/qgsnurbsutils.cpp
@@ -0,0 +1,174 @@
+/***************************************************************************
+ qgsnurbsutils.cpp
+ -----------------
+ begin : December 2025
+ copyright : (C) 2025 by Loïc Bartoletti
+ email : loic dot bartoletti at oslandia dot com
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#include "qgsnurbsutils.h"
+
+#include "qgscompoundcurve.h"
+#include "qgscurvepolygon.h"
+#include "qgsgeometrycollection.h"
+#include "qgsnurbscurve.h"
+#include "qgsvertexid.h"
+
+bool QgsNurbsUtils::containsNurbsCurve( const QgsAbstractGeometry *geom )
+{
+ if ( !geom )
+ return false;
+
+ if ( qgsgeometry_cast( geom ) )
+ return true;
+
+ if ( const QgsGeometryCollection *gc = qgsgeometry_cast( geom ) )
+ {
+ for ( int i = 0; i < gc->numGeometries(); ++i )
+ {
+ if ( containsNurbsCurve( gc->geometryN( i ) ) )
+ return true;
+ }
+ }
+
+ if ( const QgsCurvePolygon *cp = qgsgeometry_cast( geom ) )
+ {
+ if ( containsNurbsCurve( cp->exteriorRing() ) )
+ return true;
+ for ( int i = 0; i < cp->numInteriorRings(); ++i )
+ {
+ if ( containsNurbsCurve( cp->interiorRing( i ) ) )
+ return true;
+ }
+ }
+
+ if ( const QgsCompoundCurve *cc = qgsgeometry_cast( geom ) )
+ {
+ for ( int i = 0; i < cc->nCurves(); ++i )
+ {
+ if ( containsNurbsCurve( cc->curveAt( i ) ) )
+ return true;
+ }
+ }
+
+ return false;
+}
+
+const QgsNurbsCurve *QgsNurbsUtils::extractNurbsCurve( const QgsAbstractGeometry *geom )
+{
+ if ( !geom )
+ return nullptr;
+
+ if ( const QgsNurbsCurve *nurbs = qgsgeometry_cast( geom ) )
+ return nurbs;
+
+ if ( const QgsGeometryCollection *gc = qgsgeometry_cast( geom ) )
+ {
+ for ( int i = 0; i < gc->numGeometries(); ++i )
+ {
+ const QgsNurbsCurve *nurbs = extractNurbsCurve( gc->geometryN( i ) );
+ if ( nurbs )
+ return nurbs;
+ }
+ }
+
+ if ( const QgsCurvePolygon *cp = qgsgeometry_cast( geom ) )
+ {
+ const QgsNurbsCurve *nurbs = extractNurbsCurve( cp->exteriorRing() );
+ if ( nurbs )
+ return nurbs;
+ for ( int i = 0; i < cp->numInteriorRings(); ++i )
+ {
+ nurbs = extractNurbsCurve( cp->interiorRing( i ) );
+ if ( nurbs )
+ return nurbs;
+ }
+ }
+
+ if ( const QgsCompoundCurve *cc = qgsgeometry_cast( geom ) )
+ {
+ for ( int i = 0; i < cc->nCurves(); ++i )
+ {
+ const QgsNurbsCurve *nurbs = extractNurbsCurve( cc->curveAt( i ) );
+ if ( nurbs )
+ return nurbs;
+ }
+ }
+
+ return nullptr;
+}
+
+const QgsNurbsCurve *QgsNurbsUtils::findNurbsCurveForVertex( const QgsAbstractGeometry *geom, const QgsVertexId &vid, int &localIndex )
+{
+ if ( !geom )
+ return nullptr;
+
+ // Direct NURBS curve
+ if ( const QgsNurbsCurve *nurbs = qgsgeometry_cast( geom ) )
+ {
+ localIndex = vid.vertex;
+ return nurbs;
+ }
+
+ // Compound curve - find the curve containing this vertex
+ if ( const QgsCompoundCurve *compound = qgsgeometry_cast( geom ) )
+ {
+ int vertexOffset = 0;
+ for ( int i = 0; i < compound->nCurves(); ++i )
+ {
+ const QgsCurve *curve = compound->curveAt( i );
+ const int curveVertexCount = curve->numPoints();
+
+ // Check if vertex is in this curve (accounting for shared endpoints)
+ const int adjustedCount = ( i == compound->nCurves() - 1 ) ? curveVertexCount : curveVertexCount - 1;
+ if ( vid.vertex < vertexOffset + adjustedCount )
+ {
+ if ( const QgsNurbsCurve *nurbs = qgsgeometry_cast( curve ) )
+ {
+ localIndex = vid.vertex - vertexOffset;
+ return nurbs;
+ }
+ return nullptr;
+ }
+ vertexOffset += adjustedCount;
+ }
+ }
+
+ // Curve polygon - check exterior and interior rings
+ if ( const QgsCurvePolygon *polygon = qgsgeometry_cast( geom ) )
+ {
+ const QgsCurve *ring = nullptr;
+ if ( vid.ring == 0 )
+ ring = polygon->exteriorRing();
+ else if ( vid.ring > 0 && vid.ring <= polygon->numInteriorRings() )
+ ring = polygon->interiorRing( vid.ring - 1 );
+
+ if ( ring )
+ return findNurbsCurveForVertex( ring, QgsVertexId( 0, 0, vid.vertex ), localIndex );
+ }
+
+ // Geometry collection
+ if ( const QgsGeometryCollection *collection = qgsgeometry_cast( geom ) )
+ {
+ if ( vid.part >= 0 && vid.part < collection->numGeometries() )
+ {
+ return findNurbsCurveForVertex( collection->geometryN( vid.part ), QgsVertexId( 0, vid.ring, vid.vertex ), localIndex );
+ }
+ }
+
+ return nullptr;
+}
+
+QgsNurbsCurve *QgsNurbsUtils::findMutableNurbsCurveForVertex( QgsAbstractGeometry *geom, const QgsVertexId &vid, int &localIndex )
+{
+ return const_cast( findNurbsCurveForVertex( geom, vid, localIndex ) );
+}
diff --git a/src/core/geometry/qgsnurbsutils.h b/src/core/geometry/qgsnurbsutils.h
new file mode 100644
index 000000000000..418a7969f729
--- /dev/null
+++ b/src/core/geometry/qgsnurbsutils.h
@@ -0,0 +1,73 @@
+/***************************************************************************
+ qgsnurbsutils.h
+ ---------------
+ begin : December 2025
+ copyright : (C) 2025 by Loïc Bartoletti
+ email : loic dot bartoletti at oslandia dot com
+ ***************************************************************************/
+
+/***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef QGSNURBSUTILS_H
+#define QGSNURBSUTILS_H
+
+#include "qgis_core.h"
+#include "qgis_sip.h"
+
+class QgsAbstractGeometry;
+class QgsNurbsCurve;
+struct QgsVertexId;
+
+/**
+ * \ingroup core
+ * \class QgsNurbsUtils
+ * \brief Utility functions for working with NURBS curves.
+ * \since QGIS 4.0
+ */
+class CORE_EXPORT QgsNurbsUtils
+{
+ public:
+
+ /**
+ * Returns TRUE if the \a geom contains a NURBS curve (recursively).
+ */
+ static bool containsNurbsCurve( const QgsAbstractGeometry *geom );
+
+ /**
+ * Extracts the first NURBS curve found in the \a geom (recursively).
+ * Returns NULLPTR if no NURBS curve is found.
+ */
+ static const QgsNurbsCurve *extractNurbsCurve( const QgsAbstractGeometry *geom );
+
+ /**
+ * Finds the NURBS curve containing the vertex identified by \a vid.
+ *
+ * Returns the NURBS curve and sets \a localIndex to the control point index
+ * within that curve. Returns NULLPTR if the vertex is not part of a NURBS curve.
+ */
+ static const QgsNurbsCurve *findNurbsCurveForVertex(
+ const QgsAbstractGeometry *geom,
+ const QgsVertexId &vid,
+ int &localIndex SIP_OUT );
+
+ /**
+ * Mutable version of findNurbsCurveForVertex().
+ *
+ * Finds the NURBS curve containing the vertex identified by \a vid.
+ * Returns the NURBS curve and sets \a localIndex to the control point index
+ * within that curve. Returns NULLPTR if the vertex is not part of a NURBS curve.
+ */
+ static QgsNurbsCurve *findMutableNurbsCurveForVertex(
+ QgsAbstractGeometry *geom,
+ const QgsVertexId &vid,
+ int &localIndex SIP_OUT );
+};
+
+#endif // QGSNURBSUTILS_H
diff --git a/src/core/geometry/qgswkbtypes.cpp b/src/core/geometry/qgswkbtypes.cpp
index 32d92d86cf71..03ae21e8dabf 100644
--- a/src/core/geometry/qgswkbtypes.cpp
+++ b/src/core/geometry/qgswkbtypes.cpp
@@ -73,6 +73,11 @@ Q_GLOBAL_STATIC_WITH_ARGS( WkbEntries, sWkbEntries, (
{ Qgis::WkbType::CircularStringZ, WkbEntry( QLatin1String( "CircularStringZ" ), false, Qgis::WkbType::MultiCurveZ, Qgis::WkbType::CircularStringZ, Qgis::WkbType::CircularString, Qgis::GeometryType::Line, true, false ) },
{ Qgis::WkbType::CircularStringM, WkbEntry( QLatin1String( "CircularStringM" ), false, Qgis::WkbType::MultiCurveM, Qgis::WkbType::CircularStringM, Qgis::WkbType::CircularString, Qgis::GeometryType::Line, false, true ) },
{ Qgis::WkbType::CircularStringZM, WkbEntry( QLatin1String( "CircularStringZM" ), false, Qgis::WkbType::MultiCurveZM, Qgis::WkbType::CircularStringZM, Qgis::WkbType::CircularString, Qgis::GeometryType::Line, true, true ) },
+ //nurbscurve
+ { Qgis::WkbType::NurbsCurve, WkbEntry( QLatin1String( "NurbsCurve" ), false, Qgis::WkbType::MultiCurve, Qgis::WkbType::NurbsCurve, Qgis::WkbType::NurbsCurve, Qgis::GeometryType::Line, false, false ) },
+ { Qgis::WkbType::NurbsCurveZ, WkbEntry( QLatin1String( "NurbsCurveZ" ), false, Qgis::WkbType::MultiCurveZ, Qgis::WkbType::NurbsCurveZ, Qgis::WkbType::NurbsCurve, Qgis::GeometryType::Line, true, false ) },
+ { Qgis::WkbType::NurbsCurveM, WkbEntry( QLatin1String( "NurbsCurveM" ), false, Qgis::WkbType::MultiCurveM, Qgis::WkbType::NurbsCurveM, Qgis::WkbType::NurbsCurve, Qgis::GeometryType::Line, false, true ) },
+ { Qgis::WkbType::NurbsCurveZM, WkbEntry( QLatin1String( "NurbsCurveZM" ), false, Qgis::WkbType::MultiCurveZM, Qgis::WkbType::NurbsCurveZM, Qgis::WkbType::NurbsCurve, Qgis::GeometryType::Line, true, true ) },
//compoundcurve
{ Qgis::WkbType::CompoundCurve, WkbEntry( QLatin1String( "CompoundCurve" ), false, Qgis::WkbType::MultiCurve, Qgis::WkbType::CompoundCurve, Qgis::WkbType::CompoundCurve, Qgis::GeometryType::Line, false, false ) },
{ Qgis::WkbType::CompoundCurveZ, WkbEntry( QLatin1String( "CompoundCurveZ" ), false, Qgis::WkbType::MultiCurveZ, Qgis::WkbType::CompoundCurveZ, Qgis::WkbType::CompoundCurve, Qgis::GeometryType::Line, true, false ) },
@@ -180,6 +185,7 @@ QString QgsWkbTypes::translatedDisplayString( Qgis::WkbType type )
case Qgis::WkbType::MultiPolygon: return QObject::tr( "MultiPolygon" );
case Qgis::WkbType::GeometryCollection: return QObject::tr( "GeometryCollection" );
case Qgis::WkbType::CircularString: return QObject::tr( "CircularString" );
+ case Qgis::WkbType::NurbsCurve: return QObject::tr( "NurbsCurve" );
case Qgis::WkbType::CompoundCurve: return QObject::tr( "CompoundCurve" );
case Qgis::WkbType::CurvePolygon: return QObject::tr( "CurvePolygon" );
case Qgis::WkbType::MultiCurve: return QObject::tr( "MultiCurve" );
@@ -196,6 +202,7 @@ QString QgsWkbTypes::translatedDisplayString( Qgis::WkbType type )
case Qgis::WkbType::MultiPolygonZ: return QObject::tr( "MultiPolygonZ" );
case Qgis::WkbType::GeometryCollectionZ: return QObject::tr( "GeometryCollectionZ" );
case Qgis::WkbType::CircularStringZ: return QObject::tr( "CircularStringZ" );
+ case Qgis::WkbType::NurbsCurveZ: return QObject::tr( "NurbsCurveZ" );
case Qgis::WkbType::CompoundCurveZ: return QObject::tr( "CompoundCurveZ" );
case Qgis::WkbType::CurvePolygonZ: return QObject::tr( "CurvePolygonZ" );
case Qgis::WkbType::MultiCurveZ: return QObject::tr( "MultiCurveZ" );
@@ -211,6 +218,7 @@ QString QgsWkbTypes::translatedDisplayString( Qgis::WkbType type )
case Qgis::WkbType::MultiPolygonM: return QObject::tr( "MultiPolygonM" );
case Qgis::WkbType::GeometryCollectionM: return QObject::tr( "GeometryCollectionM" );
case Qgis::WkbType::CircularStringM: return QObject::tr( "CircularStringM" );
+ case Qgis::WkbType::NurbsCurveM: return QObject::tr( "NurbsCurveM" );
case Qgis::WkbType::CompoundCurveM: return QObject::tr( "CompoundCurveM" );
case Qgis::WkbType::CurvePolygonM: return QObject::tr( "CurvePolygonM" );
case Qgis::WkbType::MultiCurveM: return QObject::tr( "MultiCurveM" );
@@ -225,6 +233,7 @@ QString QgsWkbTypes::translatedDisplayString( Qgis::WkbType type )
case Qgis::WkbType::MultiPolygonZM: return QObject::tr( "MultiPolygonZM" );
case Qgis::WkbType::GeometryCollectionZM: return QObject::tr( "GeometryCollectionZM" );
case Qgis::WkbType::CircularStringZM: return QObject::tr( "CircularStringZM" );
+ case Qgis::WkbType::NurbsCurveZM: return QObject::tr( "NurbsCurveZM" );
case Qgis::WkbType::CompoundCurveZM: return QObject::tr( "CompoundCurveZM" );
case Qgis::WkbType::CurvePolygonZM: return QObject::tr( "CurvePolygonZM" );
case Qgis::WkbType::MultiCurveZM: return QObject::tr( "MultiCurveZM" );
diff --git a/src/core/geometry/qgswkbtypes.h b/src/core/geometry/qgswkbtypes.h
index aec862fd40ce..8a5ec595625e 100644
--- a/src/core/geometry/qgswkbtypes.h
+++ b/src/core/geometry/qgswkbtypes.h
@@ -133,6 +133,18 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::CircularStringZM:
return Qgis::WkbType::CircularStringZM;
+ case Qgis::WkbType::NurbsCurve:
+ return Qgis::WkbType::NurbsCurve;
+
+ case Qgis::WkbType::NurbsCurveZ:
+ return Qgis::WkbType::NurbsCurveZ;
+
+ case Qgis::WkbType::NurbsCurveM:
+ return Qgis::WkbType::NurbsCurveM;
+
+ case Qgis::WkbType::NurbsCurveZM:
+ return Qgis::WkbType::NurbsCurveZM;
+
case Qgis::WkbType::CompoundCurve:
case Qgis::WkbType::MultiCurve:
return Qgis::WkbType::CompoundCurve;
@@ -306,21 +318,25 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::CompoundCurve:
case Qgis::WkbType::CircularString:
+ case Qgis::WkbType::NurbsCurve:
case Qgis::WkbType::MultiCurve:
return Qgis::WkbType::MultiCurve;
case Qgis::WkbType::CompoundCurveZ:
case Qgis::WkbType::CircularStringZ:
+ case Qgis::WkbType::NurbsCurveZ:
case Qgis::WkbType::MultiCurveZ:
return Qgis::WkbType::MultiCurveZ;
case Qgis::WkbType::CompoundCurveM:
case Qgis::WkbType::CircularStringM:
+ case Qgis::WkbType::NurbsCurveM:
case Qgis::WkbType::MultiCurveM:
return Qgis::WkbType::MultiCurveM;
case Qgis::WkbType::CompoundCurveZM:
case Qgis::WkbType::CircularStringZM:
+ case Qgis::WkbType::NurbsCurveZM:
case Qgis::WkbType::MultiCurveZM:
return Qgis::WkbType::MultiCurveZM;
@@ -451,6 +467,7 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::LineString:
case Qgis::WkbType::CompoundCurve:
case Qgis::WkbType::CircularString:
+ case Qgis::WkbType::NurbsCurve:
return Qgis::WkbType::CompoundCurve;
case Qgis::WkbType::MultiLineString:
@@ -460,6 +477,7 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::LineStringZ:
case Qgis::WkbType::CompoundCurveZ:
case Qgis::WkbType::CircularStringZ:
+ case Qgis::WkbType::NurbsCurveZ:
case Qgis::WkbType::LineString25D:
return Qgis::WkbType::CompoundCurveZ;
@@ -471,6 +489,7 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::LineStringM:
case Qgis::WkbType::CompoundCurveM:
case Qgis::WkbType::CircularStringM:
+ case Qgis::WkbType::NurbsCurveM:
return Qgis::WkbType::CompoundCurveM;
case Qgis::WkbType::MultiLineStringM:
@@ -480,6 +499,7 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::LineStringZM:
case Qgis::WkbType::CompoundCurveZM:
case Qgis::WkbType::CircularStringZM:
+ case Qgis::WkbType::NurbsCurveZM:
return Qgis::WkbType::CompoundCurveZM;
case Qgis::WkbType::MultiLineStringZM:
@@ -557,18 +577,22 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::CircularString:
case Qgis::WkbType::CompoundCurve:
+ case Qgis::WkbType::NurbsCurve:
return Qgis::WkbType::LineString;
case Qgis::WkbType::CircularStringM:
case Qgis::WkbType::CompoundCurveM:
+ case Qgis::WkbType::NurbsCurveM:
return Qgis::WkbType::LineStringM;
case Qgis::WkbType::CircularStringZ:
case Qgis::WkbType::CompoundCurveZ:
+ case Qgis::WkbType::NurbsCurveZ:
return Qgis::WkbType::LineStringZ;
case Qgis::WkbType::CircularStringZM:
case Qgis::WkbType::CompoundCurveZM:
+ case Qgis::WkbType::NurbsCurveZM:
return Qgis::WkbType::LineStringZM;
case Qgis::WkbType::MultiCurve:
@@ -735,6 +759,12 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::CircularStringZM:
return Qgis::WkbType::CircularString;
+ case Qgis::WkbType::NurbsCurve:
+ case Qgis::WkbType::NurbsCurveZ:
+ case Qgis::WkbType::NurbsCurveM:
+ case Qgis::WkbType::NurbsCurveZM:
+ return Qgis::WkbType::NurbsCurve;
+
case Qgis::WkbType::CompoundCurve:
case Qgis::WkbType::CompoundCurveZ:
case Qgis::WkbType::CompoundCurveM:
@@ -821,6 +851,7 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::Triangle:
case Qgis::WkbType::CircularString:
case Qgis::WkbType::CompoundCurve:
+ case Qgis::WkbType::NurbsCurve:
case Qgis::WkbType::CurvePolygon:
case Qgis::WkbType::PolyhedralSurface:
case Qgis::WkbType::TIN:
@@ -831,6 +862,7 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::TriangleZ:
case Qgis::WkbType::CircularStringZ:
case Qgis::WkbType::CompoundCurveZ:
+ case Qgis::WkbType::NurbsCurveZ:
case Qgis::WkbType::CurvePolygonZ:
case Qgis::WkbType::PolyhedralSurfaceZ:
case Qgis::WkbType::TINZ:
@@ -840,6 +872,7 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::TriangleM:
case Qgis::WkbType::CircularStringM:
case Qgis::WkbType::CompoundCurveM:
+ case Qgis::WkbType::NurbsCurveM:
case Qgis::WkbType::CurvePolygonM:
case Qgis::WkbType::PolyhedralSurfaceM:
case Qgis::WkbType::TINM:
@@ -849,6 +882,7 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::TriangleZM:
case Qgis::WkbType::CircularStringZM:
case Qgis::WkbType::CompoundCurveZM:
+ case Qgis::WkbType::NurbsCurveZM:
case Qgis::WkbType::CurvePolygonZM:
case Qgis::WkbType::PolyhedralSurfaceZM:
case Qgis::WkbType::TINZM:
@@ -872,6 +906,7 @@ class CORE_EXPORT QgsWkbTypes
{
case Qgis::WkbType::CircularString:
case Qgis::WkbType::CompoundCurve:
+ case Qgis::WkbType::NurbsCurve:
case Qgis::WkbType::CurvePolygon:
case Qgis::WkbType::MultiCurve:
case Qgis::WkbType::MultiSurface:
@@ -957,15 +992,19 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::MultiLineString25D:
case Qgis::WkbType::CircularString:
case Qgis::WkbType::CompoundCurve:
+ case Qgis::WkbType::NurbsCurve:
case Qgis::WkbType::MultiCurve:
case Qgis::WkbType::CircularStringZ:
case Qgis::WkbType::CompoundCurveZ:
+ case Qgis::WkbType::NurbsCurveZ:
case Qgis::WkbType::MultiCurveZ:
case Qgis::WkbType::CircularStringM:
case Qgis::WkbType::CompoundCurveM:
+ case Qgis::WkbType::NurbsCurveM:
case Qgis::WkbType::MultiCurveM:
case Qgis::WkbType::CircularStringZM:
case Qgis::WkbType::CompoundCurveZM:
+ case Qgis::WkbType::NurbsCurveZM:
case Qgis::WkbType::MultiCurveZM:
return Qgis::GeometryType::Line;
@@ -1075,6 +1114,8 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::CurvePolygonZM:
case Qgis::WkbType::MultiCurveZM:
case Qgis::WkbType::MultiSurfaceZM:
+ case Qgis::WkbType::NurbsCurveZ:
+ case Qgis::WkbType::NurbsCurveZM:
case Qgis::WkbType::Point25D:
case Qgis::WkbType::LineString25D:
case Qgis::WkbType::Polygon25D:
@@ -1129,6 +1170,8 @@ class CORE_EXPORT QgsWkbTypes
case Qgis::WkbType::CurvePolygonZM:
case Qgis::WkbType::MultiCurveZM:
case Qgis::WkbType::MultiSurfaceZM:
+ case Qgis::WkbType::NurbsCurveM:
+ case Qgis::WkbType::NurbsCurveZM:
return true;
default:
diff --git a/src/core/providers/ogr/qgsogrproviderutils.cpp b/src/core/providers/ogr/qgsogrproviderutils.cpp
index 32c1f5f9b2ea..e50406393949 100644
--- a/src/core/providers/ogr/qgsogrproviderutils.cpp
+++ b/src/core/providers/ogr/qgsogrproviderutils.cpp
@@ -1796,6 +1796,16 @@ OGRwkbGeometryType QgsOgrProviderUtils::ogrTypeFromQgisType( Qgis::WkbType type
case Qgis::WkbType::CircularStringZM:
return wkbCircularStringZM;
+ // NURBS curves are converted to CompoundCurve for OGR compatibility
+ case Qgis::WkbType::NurbsCurve:
+ return wkbCompoundCurve;
+ case Qgis::WkbType::NurbsCurveZ:
+ return wkbCompoundCurveZ;
+ case Qgis::WkbType::NurbsCurveM:
+ return wkbCompoundCurveM;
+ case Qgis::WkbType::NurbsCurveZM:
+ return wkbCompoundCurveZM;
+
case Qgis::WkbType::CompoundCurve:
return wkbCompoundCurve;
case Qgis::WkbType::CompoundCurveZ:
diff --git a/src/core/qgis.h b/src/core/qgis.h
index 58af259b4c53..66e1aefd01c0 100644
--- a/src/core/qgis.h
+++ b/src/core/qgis.h
@@ -291,6 +291,7 @@ class CORE_EXPORT Qgis
MultiSurface = 12, //!< MultiSurface
PolyhedralSurface = 15, //!< PolyhedralSurface \since QGIS 3.40
TIN = 16, //!< TIN \since QGIS 3.40
+ NurbsCurve = 21, //!< NurbsCurve \since QGIS 4.0
NoGeometry = 100, //!< No geometry
PointZ = 1001, //!< PointZ
LineStringZ = 1002, //!< LineStringZ
@@ -307,6 +308,7 @@ class CORE_EXPORT Qgis
MultiSurfaceZ = 1012, //!< MultiSurfaceZ
PolyhedralSurfaceZ = 1015, //!< PolyhedralSurfaceZ
TINZ = 1016, //!< TINZ
+ NurbsCurveZ = 1021, //!< NurbsCurveZ \since QGIS 4.0
PointM = 2001, //!< PointM
LineStringM = 2002, //!< LineStringM
PolygonM = 2003, //!< PolygonM
@@ -322,6 +324,7 @@ class CORE_EXPORT Qgis
MultiSurfaceM = 2012, //!< MultiSurfaceM
PolyhedralSurfaceM = 2015, //!< PolyhedralSurfaceM
TINM = 2016, //!< TINM
+ NurbsCurveM = 2021, //!< NurbsCurveM \since QGIS 4.0
PointZM = 3001, //!< PointZM
LineStringZM = 3002, //!< LineStringZM
PolygonZM = 3003, //!< PolygonZM
@@ -337,6 +340,7 @@ class CORE_EXPORT Qgis
PolyhedralSurfaceZM = 3015, //!< PolyhedralSurfaceM
TINZM = 3016, //!< TINZM
TriangleZM = 3017, //!< TriangleZM
+ NurbsCurveZM = 3021, //!< NurbsCurveZM \since QGIS 4.0
Point25D = 0x80000001, //!< Point25D
LineString25D, //!< LineString25D
Polygon25D, //!< Polygon25D
@@ -399,9 +403,22 @@ class CORE_EXPORT Qgis
CircularString, //!< Capture in circular strings
Streaming, //!< Streaming points digitizing mode (points are automatically added as the mouse cursor moves).
Shape, //!< Digitize shapes.
+ NurbsCurve, //!< Digitizes NURBS curves with control points. \since QGIS 4.0
};
Q_ENUM( CaptureTechnique )
+ /**
+ * NURBS digitizing mode.
+ *
+ * \since QGIS 4.0
+ */
+ enum class NurbsMode : int
+ {
+ ControlPoints, //!< Direct control points mode - the curve is attracted to control points but does not pass through them
+ PolyBezier, //!< Poly-Bézier mode (vector graphics style) - anchors with tangent handles, the curve passes through anchor points
+ };
+ Q_ENUM( NurbsMode )
+
/**
* Vector layer type flags.
*
@@ -754,6 +771,7 @@ class CORE_EXPORT Qgis
Centroid SIP_MONKEYPATCH_COMPAT_NAME( CentroidFlag ) = 1 << 3, //!< On centroid
MiddleOfSegment SIP_MONKEYPATCH_COMPAT_NAME( MiddleOfSegmentFlag ) = 1 << 4, //!< On Middle segment
LineEndpoint SIP_MONKEYPATCH_COMPAT_NAME( LineEndpointFlag ) = 1 << 5, //!< Start or end points of lines, or first vertex in polygon rings only \since QGIS 3.20
+ ControlPoint SIP_MONKEYPATCH_COMPAT_NAME( ControlPoint ) = 1 << 6, //!< On control points (for NURBS curves) \since QGIS 4.0
};
Q_ENUM( SnappingType )
//! Snapping types
@@ -3066,6 +3084,7 @@ class CORE_EXPORT Qgis
{
Segment SIP_MONKEYPATCH_COMPAT_NAME( SegmentVertex ) = 1, //!< The actual start or end point of a segment
Curve SIP_MONKEYPATCH_COMPAT_NAME( CurveVertex ) = 2, //!< An intermediate point on a segment defining the curvature of the segment
+ ControlPoint SIP_MONKEYPATCH_COMPAT_NAME( ControlPointVertex ) = 3, //!< A NURBS control point (does not lie on the curve) \since QGIS 4.0
};
Q_ENUM( VertexType )
diff --git a/src/core/qgspointlocator.cpp b/src/core/qgspointlocator.cpp
index 22908eb0d556..589db02750f9 100644
--- a/src/core/qgspointlocator.cpp
+++ b/src/core/qgspointlocator.cpp
@@ -19,12 +19,14 @@
#include "qgis.h"
#include "qgsapplication.h"
+#include "qgscompoundcurve.h"
#include "qgscurvepolygon.h"
#include "qgsexpressioncontextutils.h"
#include "qgsfeatureiterator.h"
#include "qgsgeometry.h"
#include "qgslinestring.h"
#include "qgslogger.h"
+#include "qgsnurbscurve.h"
#include "qgspointlocatorinittask.h"
#include "qgsrendercontext.h"
#include "qgsrenderer.h"
@@ -41,7 +43,6 @@
using namespace SpatialIndex;
-
static SpatialIndex::Point point2point( const QgsPointXY &point )
{
double plow[2] = { point.x(), point.y() };
@@ -69,12 +70,16 @@ class QgsPointLocator_Stream : public IDataStream
explicit QgsPointLocator_Stream( const QLinkedList &dataList )
: mDataList( dataList )
, mIt( mDataList )
- { }
+ {}
IData *getNext() override { return mIt.next(); }
bool hasNext() override { return mIt.hasNext(); }
- uint32_t size() override { Q_ASSERT( false && "not available" ); return 0; }
+ uint32_t size() override
+ {
+ Q_ASSERT( false && "not available" );
+ return 0;
+ }
void rewind() override { Q_ASSERT( false && "not available" ); }
private:
@@ -115,7 +120,7 @@ class QgsPointLocator_VisitorNearestVertex : public IVisitor
const QgsPointXY pt = geom->closestVertex( mSrcPoint, vertexIndex, beforeVertex, afterVertex, sqrDist );
if ( sqrDist < 0 )
- return; // probably empty geometry
+ return; // probably empty geometry
const QgsPointLocator::Match m( QgsPointLocator::Vertex, mLocator->mLayer, id, std::sqrt( sqrDist ), pt, vertexIndex );
// in range queries the filter may reject some matches
@@ -134,7 +139,6 @@ class QgsPointLocator_VisitorNearestVertex : public IVisitor
};
-
/**
* \ingroup core
* \brief Helper class used when traversing the index looking for centroid - builds a list of matches.
@@ -144,7 +148,6 @@ class QgsPointLocator_VisitorNearestVertex : public IVisitor
class QgsPointLocator_VisitorNearestCentroid : public IVisitor
{
public:
-
/**
* \ingroup core
* \brief Helper class used when traversing the index looking for centroid - builds a list of matches.
@@ -177,7 +180,6 @@ class QgsPointLocator_VisitorNearestCentroid : public IVisitor
if ( !mBest.isValid() || m.distance() < mBest.distance() )
mBest = m;
-
}
private:
@@ -195,10 +197,9 @@ class QgsPointLocator_VisitorNearestCentroid : public IVisitor
* \note not available in Python bindings
* \since QGIS 3.12
*/
-class QgsPointLocator_VisitorNearestMiddleOfSegment: public IVisitor
+class QgsPointLocator_VisitorNearestMiddleOfSegment : public IVisitor
{
public:
-
/**
* \ingroup core
* \brief Helper class used when traversing the index looking for middle segment - builds a list of matches.
@@ -240,7 +241,6 @@ class QgsPointLocator_VisitorNearestMiddleOfSegment: public IVisitor
if ( !mBest.isValid() || m.distance() < mBest.distance() )
mBest = m;
-
}
private:
@@ -261,7 +261,6 @@ class QgsPointLocator_VisitorNearestMiddleOfSegment: public IVisitor
class QgsPointLocator_VisitorNearestLineEndpoint : public IVisitor
{
public:
-
/**
* \ingroup core
* \brief Helper class used when traversing the index looking for line endpoints (start or end vertex) - builds a list of matches.
@@ -357,6 +356,107 @@ class QgsPointLocator_VisitorNearestLineEndpoint : public IVisitor
};
+/**
+ * \ingroup core
+ * \class QgsPointLocator_VisitorNearestControlPoint
+ * \brief Helper class used when traversing the index looking for control points (NURBS curve)- builds a list of matches.
+ * \note not available in Python bindings
+ * \since QGIS 4.0
+*/
+class QgsPointLocator_VisitorNearestControlPoint : public IVisitor
+{
+ public:
+ /**
+ * \ingroup core
+ * \brief Helper class used when traversing the index looking for control points - builds a list of matches.
+ */
+ QgsPointLocator_VisitorNearestControlPoint( QgsPointLocator *pl, QgsPointLocator::Match &m, const QgsPointXY &srcPoint, QgsPointLocator::MatchFilter *filter = nullptr )
+ : mLocator( pl )
+ , mBest( m )
+ , mSrcPoint( srcPoint )
+ , mFilter( filter )
+ {}
+
+ void visitNode( const INode &n ) override { Q_UNUSED( n ) }
+ void visitData( std::vector &v ) override { Q_UNUSED( v ) }
+
+ void visitData( const IData &d ) override
+ {
+ const QgsFeatureId id = d.getIdentifier();
+ const QgsGeometry *geom = mLocator->mGeoms.value( id );
+ if ( !geom )
+ return; // should not happen, but be safe
+
+ QgsPointXY bestPoint;
+ int bestVertexNumber = -1;
+ auto replaceIfBetter = [this, &bestPoint, &bestVertexNumber]( const QgsPoint & candidate, int vertexNumber )
+ {
+ if ( bestPoint.isEmpty() || candidate.distanceSquared( mSrcPoint.x(), mSrcPoint.y() ) < bestPoint.sqrDist( mSrcPoint ) )
+ {
+ bestPoint = QgsPointXY( candidate );
+ bestVertexNumber = vertexNumber;
+ }
+ };
+
+ // Helper to extract control points from a curve (handles NURBS)
+ std::function extractControlPoints = [&]( const QgsCurve * curve, int &controlPointNum )
+ {
+ if ( !curve )
+ return;
+
+ if ( const QgsNurbsCurve *nurbsCurve = qgsgeometry_cast< const QgsNurbsCurve * >( curve ) )
+ {
+ const QVector controlPoints = nurbsCurve->controlPoints();
+ for ( int i = 0; i < controlPoints.size(); ++i )
+ {
+ replaceIfBetter( controlPoints[i], controlPointNum + i );
+ }
+ controlPointNum += controlPoints.size();
+ }
+ };
+
+ switch ( QgsWkbTypes::geometryType( geom->wkbType() ) )
+ {
+ case Qgis::GeometryType::Point:
+ case Qgis::GeometryType::Polygon:
+ case Qgis::GeometryType::Unknown:
+ case Qgis::GeometryType::Null:
+ return;
+
+ case Qgis::GeometryType::Line:
+ {
+ int controlPointNum = 0;
+ for ( auto partIt = geom->const_parts_begin(); partIt != geom->const_parts_end(); ++partIt )
+ {
+ if ( const QgsCurve *curve = qgsgeometry_cast< const QgsCurve * >( *partIt ) )
+ {
+ extractControlPoints( curve, controlPointNum );
+ }
+ }
+ break;
+ }
+ }
+
+ if ( bestPoint.isEmpty() )
+ return;
+
+ const QgsPointLocator::Match m( QgsPointLocator::ControlPoint, mLocator->mLayer, id, std::sqrt( mSrcPoint.sqrDist( bestPoint ) ), bestPoint, bestVertexNumber );
+ // in range queries the filter may reject some matches
+ if ( mFilter && !mFilter->acceptMatch( m ) )
+ return;
+
+ if ( !mBest.isValid() || m.distance() < mBest.distance() )
+ mBest = m;
+ }
+
+ private:
+ QgsPointLocator *mLocator = nullptr;
+ QgsPointLocator::Match &mBest;
+ QgsPointXY mSrcPoint;
+ QgsPointLocator::MatchFilter *mFilter = nullptr;
+};
+
+
////////////////////////////////////////////////////////////////////////////
@@ -447,6 +547,7 @@ class QgsPointLocator_VisitorArea : public IVisitor
mList << m;
}
}
+
private:
QgsPointLocator *mLocator = nullptr;
QgsPointLocator::MatchList &mList;
@@ -475,14 +576,14 @@ struct _CohenSutherland
OutCode computeOutCode( double x, double y )
{
- OutCode code = INSIDE; // initialized as being inside of clip window
- if ( x < mRect.xMinimum() ) // to the left of clip window
+ OutCode code = INSIDE; // initialized as being inside of clip window
+ if ( x < mRect.xMinimum() ) // to the left of clip window
code |= LEFT;
- else if ( x > mRect.xMaximum() ) // to the right of clip window
+ else if ( x > mRect.xMaximum() ) // to the right of clip window
code |= RIGHT;
- if ( y < mRect.yMinimum() ) // below the clip window
+ if ( y < mRect.yMinimum() ) // below the clip window
code |= BOTTOM;
- else if ( y > mRect.yMaximum() ) // above the clip window
+ else if ( y > mRect.yMaximum() ) // above the clip window
code |= TOP;
return code;
}
@@ -613,7 +714,6 @@ static QgsPointLocator::MatchList _geometrySegmentsInRect( QgsGeometry *geom, co
prevPoint = QgsPointXY( *it );
it++;
pointIndex += 1;
-
}
}
return lst;
@@ -644,7 +744,7 @@ class QgsPointLocator_VisitorEdgesInRect : public IVisitor
if ( !geom )
return; // should not happen, but be safe
- const auto segmentsInRect {_geometrySegmentsInRect( geom, mSrcRect, mLocator->mLayer, id )};
+ const auto segmentsInRect { _geometrySegmentsInRect( geom, mSrcRect, mLocator->mLayer, id ) };
for ( const QgsPointLocator::Match &m : segmentsInRect )
{
// in range queries the filter may reject some matches
@@ -785,7 +885,7 @@ class QgsPointLocator_VisitorMiddlesInRect : public IVisitor
if ( !geom )
return; // should not happen, but be safe
- for ( QgsAbstractGeometry::const_part_iterator itPart = geom->const_parts_begin() ; itPart != geom->const_parts_end() ; ++itPart )
+ for ( QgsAbstractGeometry::const_part_iterator itPart = geom->const_parts_begin(); itPart != geom->const_parts_end(); ++itPart )
{
QgsAbstractGeometry::vertex_iterator it = ( *itPart )->vertices_begin();
QgsAbstractGeometry::vertex_iterator itPrevious = ( *itPart )->vertices_begin();
@@ -829,7 +929,6 @@ class QgsPointLocator_DumpTree : public SpatialIndex::IQueryStrategy
QStack ids;
public:
-
void getNextEntry( const IEntry &entry, id_type &nextEntry, bool &hasNext ) override
{
const INode *n = dynamic_cast( &entry );
@@ -855,7 +954,7 @@ class QgsPointLocator_DumpTree : public SpatialIndex::IQueryStrategy
}
}
- if ( ! ids.empty() )
+ if ( !ids.empty() )
{
nextEntry = ids.back();
ids.pop();
@@ -931,7 +1030,6 @@ void QgsPointLocator::setRenderContext( const QgsRenderContext *context )
mContext = std::make_unique( *context );
connect( mLayer, &QgsVectorLayer::styleChanged, this, &QgsPointLocator::destroyIndex );
}
-
}
void QgsPointLocator::onInitTaskFinished()
@@ -1003,7 +1101,6 @@ bool QgsPointLocator::init( int maxFeaturesToIndex, bool relaxed )
void QgsPointLocator::waitForIndexingFinished()
{
-
disconnect( mInitTask, &QgsPointLocatorInitTask::taskTerminated, this, &QgsPointLocator::onInitTaskFinished );
disconnect( mInitTask, &QgsPointLocatorInitTask::taskCompleted, this, &QgsPointLocator::onInitTaskFinished );
mInitTask->waitForFinished();
@@ -1165,8 +1262,7 @@ bool QgsPointLocator::rebuildIndex( int maxFeaturesToIndex )
QgsPointLocator_Stream stream( dataList );
try
{
- mRTree.reset( RTree::createAndBulkLoadNewRTree( RTree::BLM_STR, stream, *mStorage, fillFactor, indexCapacity,
- leafCapacity, dimension, variant, indexId ) );
+ mRTree.reset( RTree::createAndBulkLoadNewRTree( RTree::BLM_STR, stream, *mStorage, fillFactor, indexCapacity, leafCapacity, dimension, variant, indexId ) );
}
catch ( const std::exception &e )
{
@@ -1311,7 +1407,6 @@ void QgsPointLocator::onFeatureDeleted( QgsFeatureId fid )
delete *it;
mGeoms.erase( it );
}
-
}
void QgsPointLocator::onGeometryChanged( QgsFeatureId fid, const QgsGeometry &geom )
@@ -1392,6 +1487,21 @@ QgsPointLocator::Match QgsPointLocator::nearestLineEndpoints( const QgsPointXY &
return m;
}
+QgsPointLocator::Match QgsPointLocator::nearestControlPoint( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter, bool relaxed )
+{
+ if ( !prepare( relaxed ) )
+ return Match();
+
+ Match m;
+ QgsPointLocator_VisitorNearestControlPoint visitor( this, m, point, filter );
+
+ const QgsRectangle rect( point.x() - tolerance, point.y() - tolerance, point.x() + tolerance, point.y() + tolerance );
+ mRTree->intersectsWithQuery( QgsSpatialIndexUtils::rectangleToRegion( rect ), visitor );
+ if ( m.isValid() && m.distance() > tolerance )
+ return Match(); // make sure that only match strictly within the tolerance is returned
+ return m;
+}
+
QgsPointLocator::Match QgsPointLocator::nearestEdge( const QgsPointXY &point, double tolerance, MatchFilter *filter, bool relaxed )
{
if ( !prepare( relaxed ) )
diff --git a/src/core/qgspointlocator.h b/src/core/qgspointlocator.h
index 267d964be86c..42f77143f3cd 100644
--- a/src/core/qgspointlocator.h
+++ b/src/core/qgspointlocator.h
@@ -85,7 +85,7 @@ namespace SpatialIndex SIP_SKIP
{
class IStorageManager;
class ISpatialIndex;
-}
+} //namespace SpatialIndex
/**
* \ingroup core
@@ -104,7 +104,6 @@ class CORE_EXPORT QgsPointLocator : public QObject
{
Q_OBJECT
public:
-
/**
* Construct point locator for a \a layer.
*
@@ -115,9 +114,7 @@ class CORE_EXPORT QgsPointLocator : public QObject
*
* If \a extent is not NULLPTR, the locator will index only a subset of the layer which falls within that extent.
*/
- explicit QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateReferenceSystem &destinationCrs = QgsCoordinateReferenceSystem(),
- const QgsCoordinateTransformContext &transformContext = QgsCoordinateTransformContext(),
- const QgsRectangle *extent = nullptr );
+ explicit QgsPointLocator( QgsVectorLayer *layer, const QgsCoordinateReferenceSystem &destinationCrs = QgsCoordinateReferenceSystem(), const QgsCoordinateTransformContext &transformContext = QgsCoordinateTransformContext(), const QgsRectangle *extent = nullptr );
~QgsPointLocator() override;
@@ -152,14 +149,15 @@ class CORE_EXPORT QgsPointLocator : public QObject
*/
enum Type SIP_ENUM_BASETYPE( IntFlag )
{
- Invalid = 0, //!< Invalid
- Vertex = 1 << 0, //!< Snapped to a vertex. Can be a vertex of the geometry or an intersection.
- Edge = 1 << 1, //!< Snapped to an edge
- Area = 1 << 2, //!< Snapped to an area
- Centroid = 1 << 3, //!< Snapped to a centroid
- MiddleOfSegment = 1 << 4, //!< Snapped to the middle of a segment
- LineEndpoint = 1 << 5, //!< Start or end points of lines only \since QGIS 3.20
- All = Vertex | Edge | Area | Centroid | MiddleOfSegment //!< Combination of all types. Note LineEndpoint is not included as endpoints made redundant by the presence of the Vertex flag.
+ Invalid = 0, //!< Invalid
+ Vertex = 1 << 0, //!< Snapped to a vertex. Can be a vertex of the geometry or an intersection.
+ Edge = 1 << 1, //!< Snapped to an edge
+ Area = 1 << 2, //!< Snapped to an area
+ Centroid = 1 << 3, //!< Snapped to a centroid
+ MiddleOfSegment = 1 << 4, //!< Snapped to the middle of a segment
+ LineEndpoint = 1 << 5, //!< Start or end points of lines only \since QGIS 3.20
+ ControlPoint = 1 << 6, //!< Snapped to a control point (for NURBS curves) \since QGIS 4.0
+ All = Vertex | Edge | Area | Centroid | MiddleOfSegment //!< Combination of all types. Note LineEndpoint and ControlPoint are not included as they have specific use cases.
};
Q_DECLARE_FLAGS( Types, Type )
@@ -223,6 +221,13 @@ class CORE_EXPORT QgsPointLocator : public QObject
*/
bool hasLineEndpoint() const { return mType == LineEndpoint; }
+ /**
+ * Returns TRUE if the Match is a control point (for NURBS curves).
+ *
+ * \since QGIS 4.0
+ */
+ bool hasControlPoint() const { return mType == ControlPoint; }
+
/**
* for vertex / edge match
* units depending on what class returns it (geom.cache: layer units, map canvas snapper: dest crs units)
@@ -318,16 +323,7 @@ class CORE_EXPORT QgsPointLocator : public QObject
// TODO c++20 - replace with = default
bool operator==( const QgsPointLocator::Match &other ) const
{
- return mType == other.mType &&
- mDist == other.mDist &&
- mPoint == other.mPoint &&
- mLayer == other.mLayer &&
- mFid == other.mFid &&
- mVertexIndex == other.mVertexIndex &&
- mEdgePoints[0] == other.mEdgePoints[0] &&
- mEdgePoints[1] == other.mEdgePoints[1] &&
- mCentroid == other.mCentroid &&
- mMiddleOfSegment == other.mMiddleOfSegment;
+ return mType == other.mType && mDist == other.mDist && mPoint == other.mPoint && mLayer == other.mLayer && mFid == other.mFid && mVertexIndex == other.mVertexIndex && mEdgePoints[0] == other.mEdgePoints[0] && mEdgePoints[1] == other.mEdgePoints[1] && mCentroid == other.mCentroid && mMiddleOfSegment == other.mMiddleOfSegment;
}
protected:
@@ -392,6 +388,14 @@ class CORE_EXPORT QgsPointLocator : public QObject
*/
Match nearestLineEndpoints( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
+ /**
+ * Find nearest control point (for NURBS curves) to the specified point - up to distance specified by tolerance
+ * Optional filter may discard unwanted matches.
+ * This method is either blocking or non blocking according to \a relaxed parameter passed
+ * \since QGIS 4.0
+ */
+ Match nearestControlPoint( const QgsPointXY &point, double tolerance, QgsPointLocator::MatchFilter *filter = nullptr, bool relaxed = false );
+
/**
* Find nearest edge to the specified point - up to distance specified by tolerance
* Optional filter may discard unwanted matches.
@@ -490,7 +494,6 @@ class CORE_EXPORT QgsPointLocator : public QObject
void onAttributeValueChanged( QgsFeatureId fid, int idx, const QVariant &value );
private:
-
/**
* prepare index if need and returns TRUE if the index is ready to be used
* \param relaxed TRUE if index build has to be non blocking
@@ -534,6 +537,7 @@ class CORE_EXPORT QgsPointLocator : public QObject
friend class QgsPointLocator_VisitorCentroidsInRect;
friend class QgsPointLocator_VisitorMiddlesInRect;
friend class QgsPointLocator_VisitorNearestLineEndpoint;
+ friend class QgsPointLocator_VisitorNearestControlPoint;
};
diff --git a/src/core/qgssnappingconfig.cpp b/src/core/qgssnappingconfig.cpp
index 9cbb33b96ae3..b6c4344a61d9 100644
--- a/src/core/qgssnappingconfig.cpp
+++ b/src/core/qgssnappingconfig.cpp
@@ -294,6 +294,8 @@ QString QgsSnappingConfig::snappingTypeToString( Qgis::SnappingType type )
return QObject::tr( "Middle of Segments" );
case Qgis::SnappingType::LineEndpoint:
return QObject::tr( "Line Endpoints" );
+ case Qgis::SnappingType::ControlPoint:
+ return QObject::tr( "Control Points" );
}
return QString();
}
@@ -316,6 +318,8 @@ QIcon QgsSnappingConfig::snappingTypeToIcon( Qgis::SnappingType type )
return QgsApplication::getThemeIcon( QStringLiteral( "/mIconSnappingMiddle.svg" ) );
case Qgis::SnappingType::LineEndpoint:
return QgsApplication::getThemeIcon( QStringLiteral( "/mIconSnappingEndpoint.svg" ) );
+ case Qgis::SnappingType::ControlPoint:
+ return QgsApplication::getThemeIcon( QStringLiteral( "/mIconSnappingControlPoint.svg" ) );
}
return QIcon();
}
diff --git a/src/core/qgssnappingutils.cpp b/src/core/qgssnappingutils.cpp
index d16b0e3eb963..4f51930cef1c 100644
--- a/src/core/qgssnappingutils.cpp
+++ b/src/core/qgssnappingutils.cpp
@@ -188,7 +188,7 @@ static void _replaceIfBetter( QgsPointLocator::Match &bestMatch, const QgsPointL
return;
// ORDER
- // LineEndpoint
+ // LineEndpoint, ControlPoint
// Vertex, Intersection
// Middle
// Centroid
@@ -204,6 +204,15 @@ static void _replaceIfBetter( QgsPointLocator::Match &bestMatch, const QgsPointL
return;
}
+ // control points -- similar priority to line endpoints
+ if ( ( bestMatch.type() & QgsPointLocator::ControlPoint ) && !( candidateMatch.type() & QgsPointLocator::ControlPoint ) )
+ return;
+ if ( candidateMatch.type() & QgsPointLocator::ControlPoint )
+ {
+ bestMatch = candidateMatch;
+ return;
+ }
+
// Second Vertex, or intersection
if ( ( bestMatch.type() & QgsPointLocator::Vertex ) && !( candidateMatch.type() & QgsPointLocator::Vertex ) )
return;
@@ -253,6 +262,10 @@ static void _updateBestMatch( QgsPointLocator::Match &bestMatch, const QgsPointX
{
_replaceIfBetter( bestMatch, loc->nearestLineEndpoints( pointMap, tolerance, filter, relaxed ), tolerance );
}
+ if ( type & QgsPointLocator::ControlPoint )
+ {
+ _replaceIfBetter( bestMatch, loc->nearestControlPoint( pointMap, tolerance, filter, relaxed ), tolerance );
+ }
}
diff --git a/src/core/settings/qgssettingsregistrycore.cpp b/src/core/settings/qgssettingsregistrycore.cpp
index d05e8c35720b..50b5f24b491f 100644
--- a/src/core/settings/qgssettingsregistrycore.cpp
+++ b/src/core/settings/qgssettingsregistrycore.cpp
@@ -47,6 +47,10 @@ const QgsSettingsEntryEnumFlag *QgsSettingsRegistryCore::sett
const QgsSettingsEntryInteger *QgsSettingsRegistryCore::settingsDigitizingStreamTolerance = new QgsSettingsEntryInteger( QStringLiteral( "stream-tolerance" ), QgsSettingsTree::sTreeDigitizing, 2 );
+const QgsSettingsEntryInteger *QgsSettingsRegistryCore::settingsDigitizingNurbsDegree = new QgsSettingsEntryInteger( QStringLiteral( "nurbs-degree" ), QgsSettingsTree::sTreeDigitizing, 3 );
+
+const QgsSettingsEntryEnumFlag *QgsSettingsRegistryCore::settingsDigitizingNurbsMode = new QgsSettingsEntryEnumFlag( QStringLiteral( "nurbs-mode" ), QgsSettingsTree::sTreeDigitizing, Qgis::NurbsMode::ControlPoints );
+
const QgsSettingsEntryInteger *QgsSettingsRegistryCore::settingsDigitizingLineWidth = new QgsSettingsEntryInteger( QStringLiteral( "line-width" ), QgsSettingsTree::sTreeDigitizing, 1 );
const QgsSettingsEntryColor *QgsSettingsRegistryCore::settingsDigitizingLineColor = new QgsSettingsEntryColor( QStringLiteral( "line-color" ), QgsSettingsTree::sTreeDigitizing, QColor( 255, 0, 0, 200 ) );
@@ -55,6 +59,10 @@ const QgsSettingsEntryDouble *QgsSettingsRegistryCore::settingsDigitizingLineCol
const QgsSettingsEntryColor *QgsSettingsRegistryCore::settingsDigitizingFillColor = new QgsSettingsEntryColor( QStringLiteral( "fill-color" ), QgsSettingsTree::sTreeDigitizing, QColor( 255, 0, 0, 30 ) );
+const QgsSettingsEntryColor *QgsSettingsRegistryCore::settingsDigitizingControlPolygonColor = new QgsSettingsEntryColor( QStringLiteral( "control-polygon-color" ), QgsSettingsTree::sTreeDigitizing, QColor( 100, 100, 100, 150 ) );
+
+const QgsSettingsEntryInteger *QgsSettingsRegistryCore::settingsDigitizingControlPolygonWidth = new QgsSettingsEntryInteger( QStringLiteral( "control-polygon-width" ), QgsSettingsTree::sTreeDigitizing, 1 );
+
const QgsSettingsEntryBool *QgsSettingsRegistryCore::settingsDigitizingLineGhost = new QgsSettingsEntryBool( QStringLiteral( "line-ghost" ), QgsSettingsTree::sTreeDigitizing, false );
const QgsSettingsEntryDouble *QgsSettingsRegistryCore::settingsDigitizingDefaultZValue = new QgsSettingsEntryDouble( QStringLiteral( "default-z-value" ), QgsSettingsTree::sTreeDigitizing, Qgis::DEFAULT_Z_COORDINATE );
diff --git a/src/core/settings/qgssettingsregistrycore.h b/src/core/settings/qgssettingsregistrycore.h
index de8d4ff3400a..4a52332bac6a 100644
--- a/src/core/settings/qgssettingsregistrycore.h
+++ b/src/core/settings/qgssettingsregistrycore.h
@@ -52,6 +52,18 @@ class CORE_EXPORT QgsSettingsRegistryCore : public QgsSettingsRegistry
//! Settings entry digitizing stream tolerance
static const QgsSettingsEntryInteger *settingsDigitizingStreamTolerance;
+ /**
+ * Settings entry digitizing NURBS curve degree
+ * \since QGIS 4.0
+ */
+ static const QgsSettingsEntryInteger *settingsDigitizingNurbsDegree;
+
+ /**
+ * Settings entry digitizing NURBS mode (ControlPoints or PolyBezier)
+ * \since QGIS 4.0
+ */
+ static const QgsSettingsEntryEnumFlag *settingsDigitizingNurbsMode;
+
//! Settings entry digitizing line width
static const QgsSettingsEntryInteger *settingsDigitizingLineWidth;
@@ -64,6 +76,18 @@ class CORE_EXPORT QgsSettingsRegistryCore : public QgsSettingsRegistry
//! Settings entry digitizing fill color
static const QgsSettingsEntryColor *settingsDigitizingFillColor;
+ /**
+ * Settings entry digitizing control polygon color (for NURBS/Bézier visualization)
+ * \since QGIS 4.0
+ */
+ static const QgsSettingsEntryColor *settingsDigitizingControlPolygonColor;
+
+ /**
+ * Settings entry digitizing control polygon width (for NURBS/Bézier visualization)
+ * \since QGIS 4.0
+ */
+ static const QgsSettingsEntryInteger *settingsDigitizingControlPolygonWidth;
+
//! Settings entry digitizing line ghost
static const QgsSettingsEntryBool *settingsDigitizingLineGhost;
diff --git a/src/gui/CMakeLists.txt b/src/gui/CMakeLists.txt
index 5255a9831203..7fe5ec92877a 100644
--- a/src/gui/CMakeLists.txt
+++ b/src/gui/CMakeLists.txt
@@ -357,8 +357,12 @@ set(QGIS_GUI_SRCS
maptools/qgsmaptoolcapture.cpp
maptools/qgsmaptoolcapturelayergeometry.cpp
maptools/qgsmaptoolcapturerubberband.cpp
- maptools/qgsmaptooldigitizefeature.cpp
+
+ maptools/qgsbezierdata.cpp
+ maptools/qgsbeziermarker.cpp
+
maptools/qgsmaptooledit.cpp
+ maptools/qgsmaptooldigitizefeature.cpp
maptools/qgsmaptooleditblanksegments.cpp
maptools/qgsmaptoolemitpoint.cpp
maptools/qgsmaptoolextent.cpp
@@ -1369,6 +1373,8 @@ set(QGIS_GUI_HDRS
maptools/qgsmaptoolcapture.h
maptools/qgsmaptoolcapturelayergeometry.h
maptools/qgsmaptoolcapturerubberband.h
+ maptools/qgsbezierdata.h
+ maptools/qgsbeziermarker.h
maptools/qgsmaptooldigitizefeature.h
maptools/qgsmaptooledit.h
maptools/qgsmaptooleditblanksegments.h
diff --git a/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp b/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp
index efd9cc9609c1..5edf6e0e69e5 100644
--- a/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp
+++ b/src/gui/annotations/qgscreateannotationitemmaptool_impl.cpp
@@ -85,6 +85,7 @@ bool QgsMapToolCaptureAnnotationItem::supportsTechnique( Qgis::CaptureTechnique
case Qgis::CaptureTechnique::CircularString:
case Qgis::CaptureTechnique::Streaming:
case Qgis::CaptureTechnique::Shape:
+ case Qgis::CaptureTechnique::NurbsCurve:
return true;
}
BUILTIN_UNREACHABLE
diff --git a/src/gui/maptools/qgsbezierdata.cpp b/src/gui/maptools/qgsbezierdata.cpp
new file mode 100644
index 000000000000..409309dc488c
--- /dev/null
+++ b/src/gui/maptools/qgsbezierdata.cpp
@@ -0,0 +1,332 @@
+/***************************************************************************
+ qgsbezierdata.cpp - Data structure for Poly-Bézier curve digitizing
+ ---------------------
+ begin : December 2025
+ copyright : (C) 2025 by Loïc Bartoletti
+ email : loic dot bartoletti at oslandia dot com
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#include "qgsbezierdata.h"
+
+#include
+
+#include "qgsgeometryutils.h"
+#include "qgsnurbscurve.h"
+
+///@cond PRIVATE
+
+const QgsAnchorWithHandles QgsBezierData::sInvalidAnchor;
+
+void QgsBezierData::addAnchor( const QgsPoint &pt )
+{
+ mData.append( QgsAnchorWithHandles( pt ) );
+}
+
+void QgsBezierData::moveAnchor( int idx, const QgsPoint &pt )
+{
+ if ( idx < 0 || idx >= mData.count() )
+ return;
+
+ QgsAnchorWithHandles &data = mData[idx];
+
+ // Calculate offset
+ const double dx = pt.x() - data.anchor.x();
+ const double dy = pt.y() - data.anchor.y();
+ const double dz = pt.is3D() ? ( pt.z() - data.anchor.z() ) : 0.0;
+
+ // Move anchor
+ data.anchor = pt;
+
+ // Move both handles relatively
+ data.leftHandle.setX( data.leftHandle.x() + dx );
+ data.leftHandle.setY( data.leftHandle.y() + dy );
+ if ( pt.is3D() )
+ data.leftHandle.setZ( data.leftHandle.z() + dz );
+
+ data.rightHandle.setX( data.rightHandle.x() + dx );
+ data.rightHandle.setY( data.rightHandle.y() + dy );
+ if ( pt.is3D() )
+ data.rightHandle.setZ( data.rightHandle.z() + dz );
+}
+
+void QgsBezierData::moveHandle( int idx, const QgsPoint &pt )
+{
+ const int anchorIdx = idx / 2;
+ if ( anchorIdx < 0 || anchorIdx >= mData.count() )
+ return;
+
+ if ( idx % 2 == 0 )
+ mData[anchorIdx].leftHandle = pt;
+ else
+ mData[anchorIdx].rightHandle = pt;
+}
+
+void QgsBezierData::insertAnchor( int segmentIdx, const QgsPoint &pt )
+{
+ if ( segmentIdx < 0 || segmentIdx > mData.count() )
+ return;
+
+ mData.insert( segmentIdx, QgsAnchorWithHandles( pt ) );
+}
+
+void QgsBezierData::deleteAnchor( int idx )
+{
+ if ( idx < 0 || idx >= mData.count() )
+ return;
+
+ mData.removeAt( idx );
+}
+
+void QgsBezierData::retractHandle( int idx )
+{
+ const int anchorIdx = idx / 2;
+ if ( anchorIdx < 0 || anchorIdx >= mData.count() )
+ return;
+
+ if ( idx % 2 == 0 )
+ mData[anchorIdx].leftHandle = mData[anchorIdx].anchor;
+ else
+ mData[anchorIdx].rightHandle = mData[anchorIdx].anchor;
+}
+
+void QgsBezierData::extendHandle( int idx, const QgsPoint &pt )
+{
+ moveHandle( idx, pt );
+}
+
+QgsPoint QgsBezierData::anchor( int idx ) const
+{
+ if ( idx < 0 || idx >= mData.count() )
+ return QgsPoint();
+ return mData[idx].anchor;
+}
+
+QgsPoint QgsBezierData::handle( int idx ) const
+{
+ const int anchorIdx = idx / 2;
+ if ( anchorIdx < 0 || anchorIdx >= mData.count() )
+ return QgsPoint();
+
+ if ( idx % 2 == 0 )
+ return mData[anchorIdx].leftHandle;
+ else
+ return mData[anchorIdx].rightHandle;
+}
+
+QVector QgsBezierData::anchors() const
+{
+ QVector result;
+ result.reserve( mData.count() );
+ for ( const QgsAnchorWithHandles &awh : mData )
+ result.append( awh.anchor );
+ return result;
+}
+
+QVector QgsBezierData::handles() const
+{
+ QVector result;
+ result.reserve( mData.count() * 2 );
+ for ( const QgsAnchorWithHandles &awh : mData )
+ {
+ result.append( awh.leftHandle );
+ result.append( awh.rightHandle );
+ }
+ return result;
+}
+
+const QgsAnchorWithHandles &QgsBezierData::anchorWithHandles( int idx ) const
+{
+ if ( idx < 0 || idx >= mData.count() )
+ return sInvalidAnchor;
+ return mData[idx];
+}
+
+QgsPointSequence QgsBezierData::interpolate() const
+{
+ QgsPointSequence result;
+
+ if ( mData.count() < 2 )
+ {
+ // Not enough anchors for a curve, just return anchors
+ for ( const QgsAnchorWithHandles &awh : mData )
+ result.append( awh.anchor );
+ return result;
+ }
+
+ // Add first anchor
+ result.append( mData.first().anchor );
+
+ // For each segment between consecutive anchors
+ for ( int i = 0; i < mData.count() - 1; ++i )
+ {
+ const QgsPoint &p0 = mData[i].anchor;
+ const QgsPoint &p1 = mData[i].rightHandle;
+ const QgsPoint &p2 = mData[i + 1].leftHandle;
+ const QgsPoint &p3 = mData[i + 1].anchor;
+
+ // Interpolate the segment
+ for ( int j = 1; j <= INTERPOLATION_POINTS; ++j )
+ {
+ const double t = static_cast( j ) / INTERPOLATION_POINTS;
+ result.append( QgsGeometryUtils::interpolatePointOnCubicBezier( p0, p1, p2, p3, t ) );
+ }
+ }
+
+ return result;
+}
+
+std::unique_ptr QgsBezierData::asNurbsCurve() const
+{
+ const int n = mData.count();
+ if ( n < 2 )
+ return nullptr;
+
+ // Build control points: anchor, handle_right, handle_left, anchor, ...
+ // A piecewise cubic Bézier with n anchors has n-1 segments
+ // Total control points: 1 + 3*(n-1) = 3n-2
+ QVector ctrlPts;
+ ctrlPts.append( mData[0].anchor );
+
+ for ( int i = 0; i < n - 1; ++i )
+ {
+ ctrlPts.append( mData[i].rightHandle );
+ ctrlPts.append( mData[i + 1].leftHandle );
+ ctrlPts.append( mData[i + 1].anchor );
+ }
+
+ // Build knot vector with multiplicity 3 at junctions for C0 continuity
+ // Format: [0,0,0,0, 1,1,1, 2,2,2, ..., n-1,n-1,n-1,n-1]
+ // Total knots: 4 + 3*(n-2) + 4 = 3n + 2
+ // Actually for n-1 segments with degree 3: ctrlPts.count() + 4 = 3n-2+4 = 3n+2
+ QVector knots;
+
+ // First 4 knots are 0
+ for ( int i = 0; i < 4; ++i )
+ knots.append( 0.0 );
+
+ // Interior knots with multiplicity 3
+ for ( int i = 1; i < n - 1; ++i )
+ {
+ for ( int j = 0; j < 3; ++j )
+ knots.append( static_cast( i ) );
+ }
+
+ // Last 4 knots are n-1
+ for ( int i = 0; i < 4; ++i )
+ knots.append( static_cast( n - 1 ) );
+
+ // Uniform weights (non-rational B-spline)
+ QVector weights( ctrlPts.count(), 1.0 );
+
+ return std::make_unique( ctrlPts, 3, knots, weights );
+}
+
+void QgsBezierData::clear()
+{
+ mData.clear();
+}
+
+int QgsBezierData::findClosestAnchor( const QgsPoint &pt, double tolerance ) const
+{
+ int closestIdx = -1;
+ double minDistSq = tolerance * tolerance;
+
+ for ( int i = 0; i < mData.count(); ++i )
+ {
+ const double dx = mData[i].anchor.x() - pt.x();
+ const double dy = mData[i].anchor.y() - pt.y();
+ const double distSq = dx * dx + dy * dy;
+ if ( distSq < minDistSq )
+ {
+ minDistSq = distSq;
+ closestIdx = i;
+ }
+ }
+
+ return closestIdx;
+}
+
+int QgsBezierData::findClosestHandle( const QgsPoint &pt, double tolerance ) const
+{
+ int closestIdx = -1;
+ double minDistSq = tolerance * tolerance;
+
+ for ( int i = 0; i < mData.count(); ++i )
+ {
+ const QgsAnchorWithHandles &awh = mData[i];
+
+ // Check left handle (index 2*i)
+ if ( !qFuzzyCompare( awh.leftHandle.x(), awh.anchor.x() ) || !qFuzzyCompare( awh.leftHandle.y(), awh.anchor.y() ) )
+ {
+ const double dx = awh.leftHandle.x() - pt.x();
+ const double dy = awh.leftHandle.y() - pt.y();
+ const double distSq = dx * dx + dy * dy;
+ if ( distSq < minDistSq )
+ {
+ minDistSq = distSq;
+ closestIdx = i * 2;
+ }
+ }
+
+ // Check right handle (index 2*i+1)
+ if ( !qFuzzyCompare( awh.rightHandle.x(), awh.anchor.x() ) || !qFuzzyCompare( awh.rightHandle.y(), awh.anchor.y() ) )
+ {
+ const double dx = awh.rightHandle.x() - pt.x();
+ const double dy = awh.rightHandle.y() - pt.y();
+ const double distSq = dx * dx + dy * dy;
+ if ( distSq < minDistSq )
+ {
+ minDistSq = distSq;
+ closestIdx = i * 2 + 1;
+ }
+ }
+ }
+
+ return closestIdx;
+}
+
+int QgsBezierData::findClosestSegment( const QgsPoint &pt, double tolerance ) const
+{
+ if ( mData.count() < 2 )
+ return -1;
+
+ int closestSegment = -1;
+ double minDistSq = tolerance * tolerance;
+
+ // Check each segment
+ for ( int i = 0; i < mData.count() - 1; ++i )
+ {
+ const QgsPoint &p0 = mData[i].anchor;
+ const QgsPoint &p1 = mData[i].rightHandle;
+ const QgsPoint &p2 = mData[i + 1].leftHandle;
+ const QgsPoint &p3 = mData[i + 1].anchor;
+
+ // Sample the curve and find minimum distance
+ for ( int j = 0; j <= INTERPOLATION_POINTS; ++j )
+ {
+ const double t = static_cast( j ) / INTERPOLATION_POINTS;
+ const QgsPoint curvePoint = QgsGeometryUtils::interpolatePointOnCubicBezier( p0, p1, p2, p3, t );
+
+ const double dx = curvePoint.x() - pt.x();
+ const double dy = curvePoint.y() - pt.y();
+ const double distSq = dx * dx + dy * dy;
+
+ if ( distSq < minDistSq )
+ {
+ minDistSq = distSq;
+ closestSegment = i;
+ }
+ }
+ }
+
+ return closestSegment;
+}
+
+///@endcond PRIVATE
diff --git a/src/gui/maptools/qgsbezierdata.h b/src/gui/maptools/qgsbezierdata.h
new file mode 100644
index 000000000000..8f0a61420450
--- /dev/null
+++ b/src/gui/maptools/qgsbezierdata.h
@@ -0,0 +1,205 @@
+/***************************************************************************
+ qgsbezierdata.h - Data structure for Poly-Bézier curve digitizing
+ ---------------------
+ begin : December 2025
+ copyright : (C) 2025 by Loïc Bartoletti
+ email : loic dot bartoletti at oslandia dot com
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+#ifndef QGSBEZIERDATA_H
+#define QGSBEZIERDATA_H
+
+#include
+
+#include "qgis_gui.h"
+#include "qgspoint.h"
+
+class QgsNurbsCurve;
+
+#define SIP_NO_FILE
+
+///@cond PRIVATE
+
+/**
+ * \brief Structure representing an anchor point with its two control handles.
+ *
+ * Each anchor has:
+ *
+ * - anchor: the point where the curve passes through
+ * - leftHandle: controls the incoming tangent (from previous segment)
+ * - rightHandle: controls the outgoing tangent (to next segment)
+ *
+ * \since QGIS 4.0
+ */
+struct GUI_EXPORT QgsAnchorWithHandles
+{
+ QgsPoint anchor; //!< Anchor point (curve passes through this)
+ QgsPoint leftHandle; //!< Left handle (controls incoming tangent)
+ QgsPoint rightHandle; //!< Right handle (controls outgoing tangent)
+
+ //! Constructor with anchor at origin, handles retracted
+ QgsAnchorWithHandles() = default;
+
+ //! Constructor with anchor position, handles retracted at anchor
+ explicit QgsAnchorWithHandles( const QgsPoint &pt )
+ : anchor( pt ), leftHandle( pt ), rightHandle( pt ) {}
+
+ //! Constructor with all points specified
+ QgsAnchorWithHandles( const QgsPoint &a, const QgsPoint &left, const QgsPoint &right )
+ : anchor( a ), leftHandle( left ), rightHandle( right ) {}
+};
+
+/**
+ * \brief Data structure for managing Poly-Bézier curve during digitizing.
+ *
+ * This class stores anchor points (where the curve passes through) and
+ * handle points (tangent control points, 2 per anchor).
+ *
+ * Handle indexing:
+ *
+ * - Handle index 2*i is the "left" handle of anchor i (controls incoming tangent)
+ * - Handle index 2*i+1 is the "right" handle of anchor i (controls outgoing tangent)
+ *
+ * \since QGIS 4.0
+ */
+class GUI_EXPORT QgsBezierData
+{
+ public:
+ //! Default constructor
+ QgsBezierData() = default;
+
+ //! Number of interpolated points per Bézier segment for visualization
+ static constexpr int INTERPOLATION_POINTS = 32;
+
+ /**
+ * Adds an anchor point at the given position.
+ * Both handles are initially placed at the anchor position.
+ * \param pt anchor point position
+ */
+ void addAnchor( const QgsPoint &pt );
+
+ /**
+ * Moves the anchor at index \a idx to the new position \a pt.
+ * Handles are moved relative to the anchor.
+ * \param idx anchor index (0-based)
+ * \param pt new position
+ */
+ void moveAnchor( int idx, const QgsPoint &pt );
+
+ /**
+ * Moves the handle at index \a idx to the new position \a pt.
+ * \param idx handle index (0-based, 2 handles per anchor)
+ * \param pt new position
+ */
+ void moveHandle( int idx, const QgsPoint &pt );
+
+ /**
+ * Inserts a new anchor point at the given segment.
+ * \param segmentIdx segment index where to insert (0 = before first segment)
+ * \param pt anchor position
+ */
+ void insertAnchor( int segmentIdx, const QgsPoint &pt );
+
+ /**
+ * Deletes the anchor at index \a idx and its associated handles.
+ * \param idx anchor index to delete
+ */
+ void deleteAnchor( int idx );
+
+ /**
+ * Retracts (collapses) the handle at index \a idx to its anchor position.
+ * \param idx handle index
+ */
+ void retractHandle( int idx );
+
+ /**
+ * Extends (expands) the handle at index \a idx from its anchor.
+ * Used when a handle is initially at the anchor position.
+ * \param idx handle index
+ * \param pt new handle position
+ */
+ void extendHandle( int idx, const QgsPoint &pt );
+
+ //! Returns the number of anchor points
+ int anchorCount() const { return mData.count(); }
+
+ //! Returns the number of handles (always 2 * anchorCount)
+ int handleCount() const { return mData.count() * 2; }
+
+ //! Returns the anchor at index \a idx
+ QgsPoint anchor( int idx ) const;
+
+ //! Returns the handle at index \a idx
+ QgsPoint handle( int idx ) const;
+
+ //! Returns all anchors (extracted from mData)
+ QVector anchors() const;
+
+ //! Returns all handles (extracted from mData, 2 per anchor)
+ QVector handles() const;
+
+ /**
+ * Returns the anchor with its handles at index \a idx.
+ * \param idx anchor index (0-based)
+ * \returns QgsAnchorWithHandles structure, or default if index is invalid
+ */
+ const QgsAnchorWithHandles &anchorWithHandles( int idx ) const;
+
+ /**
+ * Returns the interpolated points of the curve for visualization.
+ * Uses cubic Bézier interpolation between anchor points.
+ */
+ QgsPointSequence interpolate() const;
+
+ /**
+ * Converts the Poly-Bézier data to a QgsNurbsCurve.
+ * The resulting curve is a piecewise cubic Bézier represented as NURBS.
+ * \returns new QgsNurbsCurve. Returns nullptr if less than 2 anchors.
+ */
+ std::unique_ptr asNurbsCurve() const;
+
+ //! Clears all data
+ void clear();
+
+ //! Returns TRUE if there are no anchors
+ bool isEmpty() const { return mData.isEmpty(); }
+
+ /**
+ * Finds the closest anchor to the given point within tolerance.
+ * \param pt point to search from
+ * \param tolerance search tolerance
+ * \returns anchor index or -1 if not found
+ */
+ int findClosestAnchor( const QgsPoint &pt, double tolerance ) const;
+
+ /**
+ * Finds the closest handle to the given point within tolerance.
+ * \param pt point to search from
+ * \param tolerance search tolerance
+ * \returns handle index or -1 if not found
+ */
+ int findClosestHandle( const QgsPoint &pt, double tolerance ) const;
+
+ /**
+ * Finds the closest segment to the given point within tolerance.
+ * \param pt point to search from
+ * \param tolerance search tolerance
+ * \returns segment index or -1 if not found
+ */
+ int findClosestSegment( const QgsPoint &pt, double tolerance ) const;
+
+ private:
+ QVector mData; //!< Anchor points with their handles (guarantees consistency)
+ static const QgsAnchorWithHandles sInvalidAnchor; //!< Invalid anchor for out-of-bounds access
+};
+
+///@endcond PRIVATE
+
+#endif // QGSBEZIERDATA_H
diff --git a/src/gui/maptools/qgsbeziermarker.cpp b/src/gui/maptools/qgsbeziermarker.cpp
new file mode 100644
index 000000000000..5057d2291c62
--- /dev/null
+++ b/src/gui/maptools/qgsbeziermarker.cpp
@@ -0,0 +1,282 @@
+/***************************************************************************
+ qgsbeziermarker.cpp - Visualization for Poly-Bézier curve digitizing
+ ---------------------
+ begin : December 2025
+ copyright : (C) 2025 by Loïc Bartoletti
+ email : loic dot bartoletti at oslandia dot com
+ ***************************************************************************
+ * *
+ * This program is free software; you can redistribute it and/or modify *
+ * it under the terms of the GNU General Public License as published by *
+ * the Free Software Foundation; either version 2 of the License, or *
+ * (at your option) any later version. *
+ * *
+ ***************************************************************************/
+
+///@cond PRIVATE
+
+#include "qgsbeziermarker.h"
+
+#include "qgsmapcanvas.h"
+#include "qgsrubberband.h"
+#include "qgssettingsentryimpl.h"
+#include "qgssettingsregistrycore.h"
+#include "qgsvertexmarker.h"
+
+QgsBezierMarker::QgsBezierMarker( QgsMapCanvas *canvas, QObject *parent )
+ : QObject( parent )
+ , mCanvas( canvas )
+ , mCurveRubberBand( std::make_unique