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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + 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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + 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( canvas, Qgis::GeometryType::Line ) ) +{ + mCurveRubberBand->setColor( QgsSettingsRegistryCore::settingsDigitizingLineColor->value() ); + mCurveRubberBand->setWidth( QgsSettingsRegistryCore::settingsDigitizingLineWidth->value() ); +} + +QgsBezierMarker::~QgsBezierMarker() = default; + +std::unique_ptr QgsBezierMarker::createAnchorMarker() +{ + auto marker = std::make_unique( mCanvas ); + marker->setIconType( QgsVertexMarker::ICON_BOX ); + marker->setIconSize( 10 ); + const QColor snapColor = QgsSettingsRegistryCore::settingsDigitizingSnapColor->value(); + marker->setColor( snapColor ); + QColor fillColor = snapColor; + fillColor.setAlpha( 100 ); + marker->setFillColor( fillColor ); + marker->setPenWidth( 2 ); + return marker; +} + +std::unique_ptr QgsBezierMarker::createHandleMarker() +{ + auto marker = std::make_unique( mCanvas ); + marker->setIconType( QgsVertexMarker::ICON_CIRCLE ); + marker->setIconSize( 8 ); + 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->setPenWidth( 2 ); + return marker; +} + +std::unique_ptr QgsBezierMarker::createHandleLine() +{ + auto rb = std::make_unique( mCanvas, Qgis::GeometryType::Line ); + QColor lineColor = QgsSettingsRegistryCore::settingsDigitizingLineColor->value(); + lineColor.setAlpha( 150 ); + rb->setColor( lineColor ); + rb->setWidth( 1 ); + rb->setLineStyle( Qt::DashLine ); + return rb; +} + +void QgsBezierMarker::updateFromData( const QgsBezierData &data ) +{ + updateAnchorMarkers( data ); + updateHandleMarkers( data ); + updateHandleLines( data ); + updateCurve( data ); +} + +void QgsBezierMarker::updateCurve( const QgsBezierData &data ) +{ + mCurveRubberBand->reset( Qgis::GeometryType::Line ); + + if ( data.anchorCount() < 1 ) + return; + + QgsPointSequence points = data.interpolate(); + + for ( const QgsPoint &pt : std::as_const( points ) ) + { + mCurveRubberBand->addPoint( QgsPointXY( pt ) ); + } + + mCurveRubberBand->setVisible( mVisible ); +} + +void QgsBezierMarker::updateAnchorMarkers( const QgsBezierData &data ) +{ + while ( static_cast( mAnchorMarkers.size() ) < data.anchorCount() ) + { + mAnchorMarkers.push_back( createAnchorMarker() ); + } + while ( static_cast( mAnchorMarkers.size() ) > data.anchorCount() ) + { + mAnchorMarkers.pop_back(); + } + + const QColor snapColor = QgsSettingsRegistryCore::settingsDigitizingSnapColor->value(); + QColor snapFillColor = snapColor; + snapFillColor.setAlpha( 100 ); + + for ( int i = 0; i < data.anchorCount(); ++i ) + { + const QgsPoint &anchor = data.anchor( i ); + mAnchorMarkers[i]->setCenter( QgsPointXY( anchor ) ); + mAnchorMarkers[i]->setVisible( mVisible ); + + if ( i == mHighlightedAnchor ) + { + const QColor selColor = mCanvas->selectionColor(); + mAnchorMarkers[i]->setColor( selColor ); + QColor selFillColor = selColor; + selFillColor.setAlpha( 150 ); + mAnchorMarkers[i]->setFillColor( selFillColor ); + } + else + { + mAnchorMarkers[i]->setColor( snapColor ); + mAnchorMarkers[i]->setFillColor( snapFillColor ); + } + } +} + +void QgsBezierMarker::updateHandleMarkers( const QgsBezierData &data ) +{ + while ( static_cast( mHandleMarkers.size() ) < data.handleCount() ) + { + mHandleMarkers.push_back( createHandleMarker() ); + } + while ( static_cast( mHandleMarkers.size() ) > data.handleCount() ) + { + mHandleMarkers.pop_back(); + } + + 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 ); + QColor handleFillColor = handleColor; + handleFillColor.setAlpha( 100 ); + + for ( int i = 0; i < data.handleCount(); ++i ) + { + const QgsPoint &handle = data.handle( i ); + const int anchorIdx = i / 2; + if ( anchorIdx >= data.anchorCount() ) + { + mHandleMarkers[i]->setVisible( false ); + continue; + } + const QgsPoint &anchor = data.anchor( anchorIdx ); + + const bool isRetracted = qFuzzyCompare( handle.x(), anchor.x() ) && qFuzzyCompare( handle.y(), anchor.y() ); + + mHandleMarkers[i]->setCenter( QgsPointXY( handle ) ); + mHandleMarkers[i]->setVisible( mVisible && mHandlesVisible && !isRetracted ); + + if ( i == mHighlightedHandle ) + { + const QColor selColor = mCanvas->selectionColor(); + mHandleMarkers[i]->setColor( selColor ); + QColor selFillColor = selColor; + selFillColor.setAlpha( 150 ); + mHandleMarkers[i]->setFillColor( selFillColor ); + } + else + { + mHandleMarkers[i]->setColor( handleColor ); + mHandleMarkers[i]->setFillColor( handleFillColor ); + } + } +} + +void QgsBezierMarker::updateHandleLines( const QgsBezierData &data ) +{ + while ( static_cast( mHandleLines.size() ) < data.handleCount() ) + { + mHandleLines.push_back( createHandleLine() ); + } + while ( static_cast( mHandleLines.size() ) > data.handleCount() ) + { + mHandleLines.pop_back(); + } + + for ( int i = 0; i < data.handleCount(); ++i ) + { + mHandleLines[i]->reset( Qgis::GeometryType::Line ); + + const QgsPoint &handle = data.handle( i ); + const int anchorIdx = i / 2; + if ( anchorIdx >= data.anchorCount() ) + { + mHandleLines[i]->setVisible( false ); + continue; + } + const QgsPoint &anchor = data.anchor( anchorIdx ); + + const bool isRetracted = qFuzzyCompare( handle.x(), anchor.x() ) && qFuzzyCompare( handle.y(), anchor.y() ); + + if ( !isRetracted && mVisible && mHandlesVisible ) + { + mHandleLines[i]->addPoint( QgsPointXY( anchor ) ); + mHandleLines[i]->addPoint( QgsPointXY( handle ) ); + mHandleLines[i]->setVisible( true ); + } + else + { + mHandleLines[i]->setVisible( false ); + } + } +} + +void QgsBezierMarker::setVisible( bool visible ) +{ + mVisible = visible; + + for ( const auto &marker : mAnchorMarkers ) + marker->setVisible( visible ); + + for ( const auto &marker : mHandleMarkers ) + marker->setVisible( visible && mHandlesVisible ); + + for ( const auto &rb : mHandleLines ) + rb->setVisible( visible && mHandlesVisible ); + + mCurveRubberBand->setVisible( visible ); +} + +void QgsBezierMarker::setHandlesVisible( bool visible ) +{ + mHandlesVisible = visible; + + for ( const auto &marker : mHandleMarkers ) + marker->setVisible( mVisible && visible ); + + for ( const auto &rb : mHandleLines ) + rb->setVisible( mVisible && visible ); +} + +void QgsBezierMarker::clear() +{ + mAnchorMarkers.clear(); + mHandleMarkers.clear(); + mHandleLines.clear(); + + if ( mCurveRubberBand ) + mCurveRubberBand->reset( Qgis::GeometryType::Line ); + + mHighlightedAnchor = -1; + mHighlightedHandle = -1; +} + +void QgsBezierMarker::setHighlightedAnchor( int idx ) +{ + mHighlightedAnchor = idx; +} + +void QgsBezierMarker::setHighlightedHandle( int idx ) +{ + mHighlightedHandle = idx; +} + +///@endcond PRIVATE + +#include "moc_qgsbeziermarker.cpp" diff --git a/src/gui/maptools/qgsbeziermarker.h b/src/gui/maptools/qgsbeziermarker.h new file mode 100644 index 000000000000..760b3c4bf22f --- /dev/null +++ b/src/gui/maptools/qgsbeziermarker.h @@ -0,0 +1,132 @@ +/*************************************************************************** + qgsbeziermarker.h - 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. * + * * + ***************************************************************************/ + +#ifndef QGSBEZIERMARKER_H +#define QGSBEZIERMARKER_H + +#include +#include + +#include "qgis_gui.h" +#include "qgsbezierdata.h" + +#include + +class QgsMapCanvas; +class QgsRubberBand; +class QgsVertexMarker; + +#define SIP_NO_FILE + +///@cond PRIVATE + +/** + * \brief Visualization class for Poly-Bézier curve during digitizing. + * + * This class manages the visual representation of anchors, handles, + * handle lines, and the curve itself on the map canvas. + * + * \since QGIS 4.0 + */ +class GUI_EXPORT QgsBezierMarker : public QObject +{ + Q_OBJECT + + public: + /** + * Constructor. + * \param canvas the map canvas for rendering + * \param parent parent object + */ + explicit QgsBezierMarker( QgsMapCanvas *canvas, QObject *parent = nullptr ); + + //! Destructor + ~QgsBezierMarker() override; + + /** + * Updates the visualization from the given Bézier data. + * \param data the Bézier curve data + */ + void updateFromData( const QgsBezierData &data ); + + /** + * Updates the curve visualization only (optimization for mouse move). + * \param data the Bézier curve data + */ + void updateCurve( const QgsBezierData &data ); + + /** + * Sets visibility of all markers. + * \param visible whether markers should be visible + */ + void setVisible( bool visible ); + + /** + * Sets visibility of handles. + * \param visible whether handles should be visible + */ + void setHandlesVisible( bool visible ); + + //! Clears all markers + void clear(); + + /** + * Highlights an anchor. + * \param idx anchor index (-1 for none) + */ + void setHighlightedAnchor( int idx ); + + /** + * Highlights a handle. + * \param idx handle index (-1 for none) + */ + void setHighlightedHandle( int idx ); + + private: + QgsMapCanvas *mCanvas = nullptr; + + std::vector> mAnchorMarkers; + std::vector> mHandleMarkers; + std::vector> mHandleLines; + std::unique_ptr mCurveRubberBand; + + int mHighlightedAnchor = -1; + int mHighlightedHandle = -1; + + bool mVisible = true; + bool mHandlesVisible = true; + + //! Creates a new anchor marker + std::unique_ptr createAnchorMarker(); + + //! Creates a new handle marker + std::unique_ptr createHandleMarker(); + + //! Creates a new handle line rubber band + std::unique_ptr createHandleLine(); + + //! Updates anchor markers + void updateAnchorMarkers( const QgsBezierData &data ); + + //! Updates handle markers + void updateHandleMarkers( const QgsBezierData &data ); + + //! Updates handle lines + void updateHandleLines( const QgsBezierData &data ); +}; + +///@endcond PRIVATE + +#endif // QGSBEZIERMARKER_H diff --git a/src/gui/maptools/qgsmaptoolcapture.cpp b/src/gui/maptools/qgsmaptoolcapture.cpp index 1b8eaaad06bb..c7de47377e5c 100644 --- a/src/gui/maptools/qgsmaptoolcapture.cpp +++ b/src/gui/maptools/qgsmaptoolcapture.cpp @@ -20,6 +20,9 @@ #include "qgsadvanceddigitizingdockwidget.h" #include "qgsapplication.h" +#include "qgsbezierdata.h" +#include "qgsbeziermarker.h" +#include "qgscompoundcurve.h" #include "qgsexception.h" #include "qgsfeatureiterator.h" #include "qgsgeometryvalidator.h" @@ -31,9 +34,11 @@ #include "qgsmaptoolcapturerubberband.h" #include "qgsmaptoolshapeabstract.h" #include "qgsmaptoolshaperegistry.h" +#include "qgsnurbscurve.h" #include "qgspolygon.h" #include "qgsproject.h" #include "qgsrubberband.h" +#include "qgssettingsentryenumflag.h" #include "qgssettingsregistrycore.h" #include "qgssnapindicator.h" #include "qgssnappingutils.h" @@ -44,6 +49,7 @@ #include #include #include +#include #include "moc_qgsmaptoolcapture.cpp" @@ -110,6 +116,7 @@ bool QgsMapToolCapture::supportsTechnique( Qgis::CaptureTechnique technique ) co case Qgis::CaptureTechnique::CircularString: case Qgis::CaptureTechnique::Streaming: case Qgis::CaptureTechnique::Shape: + case Qgis::CaptureTechnique::NurbsCurve: return false; } BUILTIN_UNREACHABLE @@ -441,6 +448,9 @@ void QgsMapToolCapture::setCurrentCaptureTechnique( Qgis::CaptureTechnique techn case Qgis::CaptureTechnique::Shape: mLineDigitizingType = Qgis::WkbType::LineString; break; + case Qgis::CaptureTechnique::NurbsCurve: + mLineDigitizingType = Qgis::WkbType::NurbsCurve; + break; } if ( mTempRubberBand ) @@ -448,6 +458,13 @@ void QgsMapToolCapture::setCurrentCaptureTechnique( Qgis::CaptureTechnique techn mCurrentCaptureTechnique = technique; + // Emit help message for Poly-Bézier mode + if ( technique == Qgis::CaptureTechnique::NurbsCurve + && QgsSettingsRegistryCore::settingsDigitizingNurbsMode->value() == Qgis::NurbsMode::PolyBezier ) + { + emit messageEmitted( tr( "Bézier editing: click and drag to add anchor with symmetric handles, click on handle/anchor to edit, Alt+click on anchor to extend handles, right-click to finish" ), Qgis::MessageLevel::Info ); + } + if ( technique == Qgis::CaptureTechnique::Shape && mCurrentShapeMapTool && isActive() ) { clean(); @@ -476,6 +493,84 @@ void QgsMapToolCapture::setCurrentShapeMapTool( const QgsMapToolShapeMetadata *s } } +void QgsMapToolCapture::cadCanvasPressEvent( QgsMapMouseEvent *e ) +{ + // Poly-Bézier mode: handle press to add anchor and start drag + if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::NurbsCurve + && QgsSettingsRegistryCore::settingsDigitizingNurbsMode->value() == Qgis::NurbsMode::PolyBezier + && ( mode() == CaptureLine || mode() == CapturePolygon ) ) + { + if ( e->button() == Qt::LeftButton ) + { + const QgsPoint mapPoint = QgsPoint( e->mapPoint() ); + + // Initialize Bézier structures if needed + if ( !mBezierData ) + mBezierData = std::make_unique(); + if ( !mBezierMarker ) + mBezierMarker = std::make_unique( mCanvas ); + + // Calculate tolerance in map units (10 pixels) + const double tolerance = mCanvas->mapUnitsPerPixel() * 10; + + // Reset drag indices + mBezierDragAnchorIndex = -1; + mBezierDragHandleIndex = -1; + mBezierMoveAnchorIndex = -1; + + // First, check if clicking on an existing handle + const int handleIdx = mBezierData->findClosestHandle( mapPoint, tolerance ); + if ( handleIdx >= 0 ) + { + // Start dragging this handle independently + mBezierDragHandleIndex = handleIdx; + mBezierDragging = true; + mBezierMarker->setHighlightedHandle( handleIdx ); + emit messageEmitted( tr( "Bézier editing: drag to move handle, release to confirm" ), Qgis::MessageLevel::Info ); + return; + } + + // Second, check if clicking on an existing anchor + const int anchorIdx = mBezierData->findClosestAnchor( mapPoint, tolerance ); + if ( anchorIdx >= 0 ) + { + if ( e->modifiers() & Qt::AltModifier ) + { + // Alt+click on anchor: extend handles symmetrically (like creating new anchor) + mBezierDragAnchorIndex = anchorIdx; + mBezierDragging = true; + mBezierMarker->setHighlightedAnchor( anchorIdx ); + emit messageEmitted( tr( "Bézier editing: drag to extend handles symmetrically, release to confirm" ), Qgis::MessageLevel::Info ); + } + else + { + // Normal click: start moving this anchor + mBezierMoveAnchorIndex = anchorIdx; + mBezierDragging = true; + mBezierMarker->setHighlightedAnchor( anchorIdx ); + emit messageEmitted( tr( "Bézier editing: drag to move anchor, release to confirm" ), Qgis::MessageLevel::Info ); + } + return; + } + + // Otherwise, add new anchor and start symmetric handle drag + mBezierData->addAnchor( mapPoint ); + mBezierDragAnchorIndex = mBezierData->anchorCount() - 1; + mBezierDragging = true; + emit messageEmitted( tr( "Bézier editing: drag to define handles, release to confirm anchor" ), Qgis::MessageLevel::Info ); + + // Update visualization + mBezierMarker->updateFromData( *mBezierData ); + + startCapturing(); + return; + } + } + + // Default handling for other modes + QgsMapToolAdvancedDigitizing::cadCanvasPressEvent( e ); +} + void QgsMapToolCapture::cadCanvasMoveEvent( QgsMapMouseEvent *e ) { // If we are adding a record to a non-spatial layer, just return @@ -507,6 +602,45 @@ void QgsMapToolCapture::cadCanvasMoveEvent( QgsMapMouseEvent *e ) return; } } + else if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::NurbsCurve + && QgsSettingsRegistryCore::settingsDigitizingNurbsMode->value() == Qgis::NurbsMode::PolyBezier ) + { + // Poly-Bézier mode handling + const QgsPoint mapPoint = QgsPoint( point ); + + if ( mBezierDragging && mBezierData ) + { + if ( mBezierDragHandleIndex >= 0 ) + { + // Dragging an existing handle independently + mBezierData->moveHandle( mBezierDragHandleIndex, mapPoint ); + } + else if ( mBezierMoveAnchorIndex >= 0 ) + { + // Moving an existing anchor (handles move relatively) + mBezierData->moveAnchor( mBezierMoveAnchorIndex, mapPoint ); + } + else if ( mBezierDragAnchorIndex >= 0 ) + { + // Creating new anchor: update both handles symmetrically (like BezierEditing's move_handle2) + const QgsPoint &anchor = mBezierData->anchor( mBezierDragAnchorIndex ); + const int leftHandleIdx = mBezierDragAnchorIndex * 2; + const int rightHandleIdx = mBezierDragAnchorIndex * 2 + 1; + + // Right handle follows mouse + mBezierData->moveHandle( rightHandleIdx, mapPoint ); + + // Left handle goes opposite direction: anchor - (mouse - anchor) = 2*anchor - mouse + const QgsPoint leftHandle( anchor.x() * 2 - mapPoint.x(), anchor.y() * 2 - mapPoint.y() ); + mBezierData->moveHandle( leftHandleIdx, leftHandle ); + } + + // Update visualization + if ( mBezierMarker ) + mBezierMarker->updateFromData( *mBezierData ); + } + return; + } else { const QgsPoint mapPoint = QgsPoint( point ); @@ -871,8 +1005,39 @@ void QgsMapToolCapture::undo( bool isAutoRepeat ) { mTracingStartPoint = QgsPointXY(); + // Handle Poly-Bézier mode: delete the last anchor with its handles + // This must be checked before the standard size() check since Poly-Bézier + // doesn't use mCaptureCurve during capture + if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::NurbsCurve + && QgsSettingsRegistryCore::settingsDigitizingNurbsMode->value() == Qgis::NurbsMode::PolyBezier + && mBezierData && mBezierData->anchorCount() > 0 ) + { + mBezierData->deleteAnchor( mBezierData->anchorCount() - 1 ); + if ( mBezierMarker ) + mBezierMarker->updateFromData( *mBezierData ); + // Reset drag state + mBezierDragging = false; + mBezierDragAnchorIndex = -1; + mBezierDragHandleIndex = -1; + mBezierMoveAnchorIndex = -1; + mCadDockWidget->removePreviousPoint(); + return; + } + if ( mTempRubberBand ) { + // Handle NURBS ControlPoints mode: remove last control point + // This must be checked before the standard size() check since NURBS ControlPoints + // doesn't use mCaptureCurve during capture + if ( mTempRubberBand->stringType() == Qgis::WkbType::NurbsCurve && mTempRubberBand->pointsCount() > 1 ) + { + const QgsPoint lastPoint = mTempRubberBand->lastPoint(); + mTempRubberBand->removeLastPoint(); + mTempRubberBand->movePoint( lastPoint ); + mCadDockWidget->removePreviousPoint(); + return; + } + if ( size() <= 1 && mTempRubberBand->pointsCount() != 0 ) return; @@ -990,6 +1155,67 @@ void QgsMapToolCapture::keyPressEvent( QKeyEvent *e ) // Override default shortcut management in MapCanvas e->ignore(); } + else if ( e->key() == Qt::Key_W && !e->isAutoRepeat() ) + { + // Enable NURBS weight editing mode when W is pressed + if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::NurbsCurve && mTempRubberBand && mTempRubberBand->pointsCount() >= 2 ) + { + mWeightEditMode = true; + // Edit the last control point by default (the one being digitized) + mWeightEditControlPointIndex = mTempRubberBand->pointsCount() - 2; // -2 because last point is the cursor position + emit messageEmitted( tr( "NURBS weight editing: Use mouse wheel to adjust weight (Ctrl=fine, Shift=coarse). Current weight: %1" ).arg( mTempRubberBand->weight( mWeightEditControlPointIndex ), 0, 'f', 2 ), Qgis::MessageLevel::Info ); + e->ignore(); + } + } +} + +void QgsMapToolCapture::keyReleaseEvent( QKeyEvent *e ) +{ + if ( e->key() == Qt::Key_W && !e->isAutoRepeat() ) + { + if ( mWeightEditMode ) + { + mWeightEditMode = false; + mWeightEditControlPointIndex = -1; + emit messageEmitted( QString() ); // Clear the message + e->accept(); + return; + } + } + + QgsMapToolAdvancedDigitizing::keyReleaseEvent( e ); +} + +void QgsMapToolCapture::wheelEvent( QWheelEvent *e ) +{ + if ( mWeightEditMode && mWeightEditControlPointIndex >= 0 && mTempRubberBand ) + { + // Calculate step based on modifiers + double step = 0.1; + if ( e->modifiers() & Qt::ControlModifier ) + step = 0.01; // Fine adjustment + else if ( e->modifiers() & Qt::ShiftModifier ) + step = 1.0; // Coarse adjustment + + // Direction based on wheel delta + const int delta = e->angleDelta().y(); + const double adjustment = ( delta > 0 ) ? step : -step; + + // Get current weight and apply adjustment + const double currentWeight = mTempRubberBand->weight( mWeightEditControlPointIndex ); + const double newWeight = std::max( 0.01, currentWeight + adjustment ); + + // Apply the new weight + if ( mTempRubberBand->setWeight( mWeightEditControlPointIndex, newWeight ) ) + { + emit messageEmitted( tr( "NURBS weight: %1" ).arg( newWeight, 0, 'f', 2 ), Qgis::MessageLevel::Info ); + } + + e->accept(); + return; + } + + QgsMapToolAdvancedDigitizing::wheelEvent( e ); } void QgsMapToolCapture::startCapturing() @@ -1021,6 +1247,15 @@ void QgsMapToolCapture::stopCapturing() mCaptureCurve.clear(); updateExtraSnapLayer(); mSnappingMatches.clear(); + + // Clean up Bézier digitizing data + if ( mBezierMarker ) + mBezierMarker->clear(); + mBezierData.reset(); + mBezierMarker.reset(); + mBezierDragging = false; + mBezierDragAnchorIndex = -1; + if ( auto *lCurrentVectorLayer = currentVectorLayer() ) lCurrentVectorLayer->triggerRepaint(); } @@ -1237,16 +1472,94 @@ void QgsMapToolCapture::updateExtraSnapLayer() if ( !mExtraSnapLayer ) return; - if ( canvas()->snappingUtils()->config().selfSnapping() && layer() && mCaptureCurve.numPoints() >= 2 ) + if ( canvas()->snappingUtils()->config().selfSnapping() && layer() ) { // the current layer may have changed mExtraSnapLayer->setCrs( layer()->crs() ); - QgsGeometry geom = QgsGeometry( mCaptureCurve.clone() ); - // we close the curve to allow snapping on last segment - if ( mCaptureMode == CapturePolygon && mCaptureCurve.numPoints() >= 3 ) + + QgsGeometry geom; + + // For NURBS curves, include both the evaluated curve and control points for snapping + if ( mLineDigitizingType == Qgis::WkbType::NurbsCurve && mTempRubberBand && mTempRubberBand->pointsCount() >= 2 ) + { + // Create a LineString that includes both the interpolated curve points AND control points + auto lineForSnap = std::make_unique(); + + // First, add all control points from the rubber band (for snapping to control points) + const int pointCount = mTempRubberBand->pointsCount(); + // Exclude the last point (cursor position) + for ( int i = 0; i < pointCount - 1; ++i ) + { + lineForSnap->addVertex( mTempRubberBand->pointFromEnd( pointCount - 1 - i ) ); + } + + // Then, try to get the evaluated curve and add its vertices (for snapping to the curve) + std::unique_ptr nurbsCurve( mTempRubberBand->curve() ); + if ( nurbsCurve ) + { + std::unique_ptr curvePoints( nurbsCurve->curveToLine() ); + if ( curvePoints ) + { + for ( int i = 0; i < curvePoints->numPoints(); ++i ) + { + lineForSnap->addVertex( curvePoints->pointN( i ) ); + } + } + } + + // For polygon mode, close the curve to allow snapping to first point + if ( mCaptureMode == CapturePolygon && lineForSnap->numPoints() >= 3 ) + { + lineForSnap->close(); + } + + geom = QgsGeometry( lineForSnap.release() ); + } + else if ( mBezierData && mBezierData->anchorCount() >= 2 ) + { + // Poly-Bézier mode: include anchors, handles, and interpolated curve for snapping + auto lineForSnap = std::make_unique(); + + // Add all anchors for snapping + const QVector anchors = mBezierData->anchors(); + for ( const QgsPoint &pt : anchors ) + { + lineForSnap->addVertex( pt ); + } + + // Add all handles for snapping + const QVector handles = mBezierData->handles(); + for ( const QgsPoint &pt : handles ) + { + lineForSnap->addVertex( pt ); + } + + // Add interpolated curve points for snapping to the curve itself + const QgsPointSequence interpolated = mBezierData->interpolate(); + for ( const QgsPoint &pt : interpolated ) + { + lineForSnap->addVertex( pt ); + } + + // For polygon mode, close the curve to allow snapping to first point + if ( mCaptureMode == CapturePolygon && lineForSnap->numPoints() >= 3 ) + { + lineForSnap->close(); + } + + geom = QgsGeometry( lineForSnap.release() ); + } + else if ( mCaptureCurve.numPoints() >= 2 ) { - qgsgeometry_cast( geom.get() )->close(); + // Standard capture curve + geom = QgsGeometry( mCaptureCurve.clone() ); + // we close the curve to allow snapping on last segment + if ( mCaptureMode == CapturePolygon && mCaptureCurve.numPoints() >= 3 ) + { + qgsgeometry_cast( geom.get() )->close(); + } } + mExtraSnapLayer->changeGeometry( mExtraSnapFeatureId, geom ); } else @@ -1324,8 +1637,118 @@ void QgsMapToolCapture::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) else if ( mode() == CaptureLine || mode() == CapturePolygon ) { bool digitizingFinished = false; + QgsPointSequence nurbsControlPoints; + QVector nurbsWeights; - if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::Shape ) + // Poly-Bézier mode handling + if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::NurbsCurve + && QgsSettingsRegistryCore::settingsDigitizingNurbsMode->value() == Qgis::NurbsMode::PolyBezier ) + { + if ( e->button() == Qt::LeftButton ) + { + // End dragging on mouse release + mBezierDragging = false; + mBezierDragAnchorIndex = -1; + mBezierDragHandleIndex = -1; + mBezierMoveAnchorIndex = -1; + + // Clear highlights + if ( mBezierMarker ) + { + mBezierMarker->setHighlightedAnchor( -1 ); + mBezierMarker->setHighlightedHandle( -1 ); + mBezierMarker->updateFromData( *mBezierData ); + } + + // Emit help message reminder + emit messageEmitted( tr( "Bézier editing: click and drag to add anchor, click on handle/anchor to edit, Alt+click to extend handles, right-click to finish" ), Qgis::MessageLevel::Info ); + return; + } + else if ( e->button() == Qt::RightButton ) + { + // End dragging + mBezierDragging = false; + mBezierDragAnchorIndex = -1; + mBezierDragHandleIndex = -1; + mBezierMoveAnchorIndex = -1; + + if ( mBezierData && mBezierData->anchorCount() >= 2 ) + { + // Convert Poly-Bézier to NurbsCurve + std::unique_ptr nurbsCurve = mBezierData->asNurbsCurve(); + if ( nurbsCurve ) + { + // Transform to layer coordinates + QgsVectorLayer *vlayer = qobject_cast( layer() ); + if ( vlayer ) + { + const QgsCoordinateTransform ct = mCanvas->mapSettings().layerTransform( vlayer ); + if ( ct.isValid() && !ct.isShortCircuited() ) + { + try + { + nurbsCurve->transform( ct, Qgis::TransformDirection::Reverse ); + } + catch ( QgsCsException & ) + { + emit messageEmitted( tr( "Cannot transform the geometry to layer coordinates" ), Qgis::MessageLevel::Warning ); + stopCapturing(); + return; + } + } + + std::unique_ptr curveToAdd; + + // Close for polygon if needed + if ( mode() == CapturePolygon && !nurbsCurve->isClosed() ) + { + // For polygon, wrap in compound curve and add closing segment + auto compound = std::make_unique(); + compound->addCurve( nurbsCurve.release() ); + // Add closing line segment from end to start + auto closingSegment = std::make_unique(); + closingSegment->addVertex( compound->endPoint() ); + closingSegment->addVertex( compound->startPoint() ); + compound->addCurve( closingSegment.release() ); + curveToAdd = std::move( compound ); + } + else + { + curveToAdd.reset( nurbsCurve.release() ); + } + QgsGeometry g; + + if ( mode() == CaptureLine ) + { + g = QgsGeometry( curveToAdd->clone() ); + geometryCaptured( g ); + lineCaptured( curveToAdd.release() ); + } + else // CapturePolygon + { + auto poly = std::make_unique(); + poly->setExteriorRing( curveToAdd.release() ); + g = QgsGeometry( poly->clone() ); + geometryCaptured( g ); + polygonCaptured( poly.get() ); + } + + digitizingFinished = true; + } + } + } + + // Clean up Bézier data + if ( mBezierMarker ) + mBezierMarker->clear(); + mBezierData.reset(); + mBezierMarker.reset(); + stopCapturing(); + return; + } + return; + } + else if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::Shape ) { if ( !mCurrentShapeMapTool ) { @@ -1364,25 +1787,78 @@ void QgsMapToolCapture::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) else if ( e->button() == Qt::RightButton ) { // End of string - deleteTempRubberBand(); - //lines: bail out if there are not at least two vertices - if ( mode() == CaptureLine && size() < 2 ) + // Extract NURBS control points and weights from the rubberband + if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::NurbsCurve && mTempRubberBand ) { - stopCapturing(); - return; + const int rbPointCount = mTempRubberBand->pointsCount(); + if ( rbPointCount > 1 ) + { + // Exclude the last point (cursor position) + for ( int i = 0; i < rbPointCount - 1; ++i ) + { + nurbsControlPoints.append( mTempRubberBand->pointFromEnd( rbPointCount - 1 - i ) ); + } + // Also extract weights (in correct order) + const QVector &rbWeights = mTempRubberBand->weights(); + for ( int i = 0; i < rbPointCount - 1; ++i ) + { + if ( i < rbWeights.size() ) + nurbsWeights.append( rbWeights[i] ); + else + nurbsWeights.append( 1.0 ); + } + } } - //polygons: bail out if there are not at least two vertices - if ( mode() == CapturePolygon && size() < 3 ) + deleteTempRubberBand(); + + if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::NurbsCurve ) { - stopCapturing(); - return; + // Minimum 4 control points required for degree 3 NURBS + if ( mode() == CaptureLine && nurbsControlPoints.count() < 4 ) + { + stopCapturing(); + return; + } + if ( mode() == CapturePolygon && nurbsControlPoints.count() < 4 ) + { + stopCapturing(); + return; + } + } + else + { + //lines: bail out if there are not at least two vertices + if ( mode() == CaptureLine && size() < 2 ) + { + stopCapturing(); + return; + } + + //polygons: bail out if there are not at least two vertices + if ( mode() == CapturePolygon && size() < 3 ) + { + stopCapturing(); + return; + } } if ( mode() == CapturePolygon || e->modifiers() == Qt::ShiftModifier ) { - closePolygon(); + // Close NURBS curve by adding first control point at the end + if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::NurbsCurve && !nurbsControlPoints.isEmpty() ) + { + // Add the first control point as the last to close the curve + nurbsControlPoints.append( nurbsControlPoints.first() ); + // Also duplicate the first weight for closure + if ( !nurbsWeights.isEmpty() ) + nurbsWeights.append( nurbsWeights.first() ); + } + else + { + closePolygon(); + } } digitizingFinished = true; @@ -1392,7 +1868,52 @@ void QgsMapToolCapture::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) if ( digitizingFinished ) { QgsGeometry g; - std::unique_ptr curveToAdd( captureCurve()->clone() ); + std::unique_ptr curveToAdd; + + // Create a single NurbsCurve from all control points + if ( mCurrentCaptureTechnique == Qgis::CaptureTechnique::NurbsCurve ) + { + const int degree = 3; + const int n = nurbsControlPoints.count(); + + if ( n >= degree + 1 ) + { + // Generate uniform clamped knot vector + const int knotCount = n + degree + 1; + QVector knots( knotCount ); + + for ( int i = 0; i <= degree; ++i ) + knots[i] = 0.0; + for ( int i = knotCount - degree - 1; i < knotCount; ++i ) + knots[i] = 1.0; + const int numMiddleKnots = n - degree - 1; + for ( int i = 0; i < numMiddleKnots; ++i ) + knots[degree + 1 + i] = static_cast( i + 1 ) / ( numMiddleKnots + 1 ); + + // Use weights from rubber band, or default to 1.0 + QVector weights; + if ( nurbsWeights.size() >= n ) + { + weights = nurbsWeights.mid( 0, n ); + } + else + { + weights = nurbsWeights; + while ( weights.size() < n ) + weights.append( 1.0 ); + } + curveToAdd.reset( new QgsNurbsCurve( nurbsControlPoints, degree, knots, weights ) ); + } + else + { + // Not enough points for NURBS, fallback to linestring + curveToAdd.reset( new QgsLineString( nurbsControlPoints ) ); + } + } + else + { + curveToAdd.reset( captureCurve()->clone() ); + } if ( mode() == CaptureLine ) { @@ -1402,27 +1923,33 @@ void QgsMapToolCapture::cadCanvasReleaseEvent( QgsMapMouseEvent *e ) } else { - //does compoundcurve contain circular strings? - //does provider support circular strings? - if ( QgsVectorLayer *vlayer = qobject_cast( layer() ) ) + // For NURBS curves, keep the already-created curve + // For other curves, check provider support for curved segments + if ( mCurrentCaptureTechnique != Qgis::CaptureTechnique::NurbsCurve ) { - const bool hasCurvedSegments = captureCurve()->hasCurvedSegments(); - const bool providerSupportsCurvedSegments = vlayer->dataProvider()->capabilities() & Qgis::VectorProviderCapability::CircularGeometries; - - if ( hasCurvedSegments && providerSupportsCurvedSegments ) + //does compoundcurve contain circular strings? + //does provider support circular strings? + if ( QgsVectorLayer *vlayer = qobject_cast( layer() ) ) { - curveToAdd.reset( captureCurve()->clone() ); + const bool hasCurvedSegments = captureCurve()->hasCurvedSegments(); + const bool providerSupportsCurvedSegments = vlayer->dataProvider()->capabilities() & Qgis::VectorProviderCapability::CircularGeometries; + + if ( hasCurvedSegments && providerSupportsCurvedSegments ) + { + curveToAdd.reset( captureCurve()->clone() ); + } + else + { + curveToAdd.reset( captureCurve()->curveToLine() ); + } } else { - curveToAdd.reset( captureCurve()->curveToLine() ); + curveToAdd.reset( captureCurve()->clone() ); } } - else - { - curveToAdd.reset( captureCurve()->clone() ); - } - std::unique_ptr poly { new QgsCurvePolygon() }; + // curveToAdd is already set for NURBS curves, so we just use it + auto poly = std::make_unique(); poly->setExteriorRing( curveToAdd.release() ); g = QgsGeometry( poly->clone() ); geometryCaptured( g ); diff --git a/src/gui/maptools/qgsmaptoolcapture.h b/src/gui/maptools/qgsmaptoolcapture.h index b85c4fb0f2a8..365db52bcc74 100644 --- a/src/gui/maptools/qgsmaptoolcapture.h +++ b/src/gui/maptools/qgsmaptoolcapture.h @@ -17,6 +17,8 @@ #define QGSMAPTOOLCAPTURE_H +#include + #include "qgis_gui.h" #include "qgscompoundcurve.h" #include "qgsgeometry.h" @@ -36,6 +38,8 @@ class QgsMapToolCaptureRubberBand; class QgsCurvePolygon; class QgsMapToolShapeAbstract; class QgsMapToolShapeMetadata; +class QgsBezierData; +class QgsBezierMarker; /** @@ -138,6 +142,7 @@ class GUI_EXPORT QgsMapToolCapture : public QgsMapToolAdvancedDigitizing */ QList snappingMatches() const; + void cadCanvasPressEvent( QgsMapMouseEvent *e ) override; void cadCanvasMoveEvent( QgsMapMouseEvent *e ) override; void cadCanvasReleaseEvent( QgsMapMouseEvent *e ) override; @@ -147,6 +152,20 @@ class GUI_EXPORT QgsMapToolCapture : public QgsMapToolAdvancedDigitizing */ void keyPressEvent( QKeyEvent *e ) override; + /** + * Intercept key release events for NURBS weight editing mode + * \param e key event + * \since QGIS 4.0 + */ + void keyReleaseEvent( QKeyEvent *e ) override; + + /** + * Intercept wheel events for NURBS weight adjustment + * \param e wheel event + * \since QGIS 4.0 + */ + void wheelEvent( QWheelEvent *e ) override; + /** * Clean a temporary rubberband */ @@ -448,6 +467,24 @@ class GUI_EXPORT QgsMapToolCapture : public QgsMapToolAdvancedDigitizing bool mIgnoreSubsequentAutoRepeatUndo = false; + //! Data structure for Poly-Bézier curve digitizing (anchors and handles) + std::unique_ptr mBezierData; + //! Visualization for Poly-Bézier curve digitizing + std::unique_ptr mBezierMarker; + //! TRUE if user is currently dragging + bool mBezierDragging = false; + //! Index of the anchor being dragged for new anchor handle definition (-1 if not) + int mBezierDragAnchorIndex = -1; + //! Index of the handle being dragged independently (-1 if not) + int mBezierDragHandleIndex = -1; + //! Index of the anchor being moved (-1 if not) + int mBezierMoveAnchorIndex = -1; + + //! TRUE if W key is held for NURBS weight editing mode + bool mWeightEditMode = false; + //! Index of the control point being edited for weight (-1 if none) + int mWeightEditControlPointIndex = -1; + friend class TestQgsMapToolCapture; }; diff --git a/src/gui/maptools/qgsmaptoolcapturerubberband.cpp b/src/gui/maptools/qgsmaptoolcapturerubberband.cpp index ca511be1dc44..4cbebf1edba9 100644 --- a/src/gui/maptools/qgsmaptoolcapturerubberband.cpp +++ b/src/gui/maptools/qgsmaptoolcapturerubberband.cpp @@ -16,16 +16,28 @@ #include "qgsmaptoolcapturerubberband.h" #include "qgsgeometryrubberband.h" +#include "qgsmaptooledit.h" +#include "qgsnurbscurve.h" +#include "qgsrubberband.h" +#include "qgssettingsentryimpl.h" +#include "qgssettingsregistrycore.h" ///@cond PRIVATE QgsMapToolCaptureRubberBand::QgsMapToolCaptureRubberBand( QgsMapCanvas *mapCanvas, Qgis::GeometryType geomType ) : QgsGeometryRubberBand( mapCanvas, geomType ) + , mControlPolygonRubberBand( std::make_unique( mapCanvas, Qgis::GeometryType::Line ) ) { setVertexDrawingEnabled( false ); + + // Style control polygon rubberband for NURBS visualization + QgsMapToolEdit::applyControlPolygonStyle( mControlPolygonRubberBand.get() ); + mControlPolygonRubberBand->setVisible( false ); } +QgsMapToolCaptureRubberBand::~QgsMapToolCaptureRubberBand() = default; + QgsCurve *QgsMapToolCaptureRubberBand::curve() { if ( mPoints.empty() ) @@ -37,7 +49,7 @@ QgsCurve *QgsMapToolCaptureRubberBand::curve() return new QgsLineString( mPoints ); break; case Qgis::WkbType::CircularString: - if ( mPoints.count() != 3 ) + if ( mPoints.size() != 3 ) return nullptr; return new QgsCircularString( mPoints[0], @@ -45,6 +57,11 @@ QgsCurve *QgsMapToolCaptureRubberBand::curve() mPoints[2] ); break; + case Qgis::WkbType::NurbsCurve: + if ( mPoints.size() < 4 ) + return nullptr; + return createNurbsCurve(); + break; default: return nullptr; } @@ -52,7 +69,8 @@ QgsCurve *QgsMapToolCaptureRubberBand::curve() bool QgsMapToolCaptureRubberBand::curveIsComplete() const { - return ( mStringType == Qgis::WkbType::LineString && mPoints.count() > 1 ) || ( mStringType == Qgis::WkbType::CircularString && mPoints.count() > 2 ); + return ( mStringType == Qgis::WkbType::LineString && mPoints.size() > 1 ) + || ( mStringType == Qgis::WkbType::CircularString && mPoints.size() > 2 ); } void QgsMapToolCaptureRubberBand::reset( Qgis::GeometryType geomType, Qgis::WkbType stringType, const QgsPoint &firstPolygonPoint ) @@ -61,6 +79,7 @@ void QgsMapToolCaptureRubberBand::reset( Qgis::GeometryType geomType, Qgis::WkbT return; mPoints.clear(); + mWeights.clear(); mFirstPolygonPoint = firstPolygonPoint; setStringType( stringType ); setRubberBandGeometryType( geomType ); @@ -74,10 +93,14 @@ void QgsMapToolCaptureRubberBand::setRubberBandGeometryType( Qgis::GeometryType void QgsMapToolCaptureRubberBand::addPoint( const QgsPoint &point, bool doUpdate ) { - if ( mPoints.count() == 0 ) + if ( mPoints.size() == 0 ) + { mPoints.append( point ); + mWeights.append( 1.0 ); + } mPoints.append( point ); + mWeights.append( 1.0 ); if ( doUpdate ) updateCurve(); @@ -85,7 +108,7 @@ void QgsMapToolCaptureRubberBand::addPoint( const QgsPoint &point, bool doUpdate void QgsMapToolCaptureRubberBand::movePoint( const QgsPoint &point ) { - if ( mPoints.count() > 0 ) + if ( mPoints.size() > 0 ) mPoints.last() = point; updateCurve(); @@ -93,13 +116,13 @@ void QgsMapToolCaptureRubberBand::movePoint( const QgsPoint &point ) void QgsMapToolCaptureRubberBand::movePoint( int index, const QgsPoint &point ) { - if ( mPoints.count() > 0 && mPoints.size() > index ) + if ( mPoints.size() > 0 && mPoints.size() > index ) mPoints[index] = point; updateCurve(); } -int QgsMapToolCaptureRubberBand::pointsCount() +int QgsMapToolCaptureRubberBand::pointsCount() const { return mPoints.size(); } @@ -111,16 +134,16 @@ Qgis::WkbType QgsMapToolCaptureRubberBand::stringType() const void QgsMapToolCaptureRubberBand::setStringType( Qgis::WkbType type ) { - if ( ( type != Qgis::WkbType::CircularString && type != Qgis::WkbType::LineString ) || type == mStringType ) + if ( ( type != Qgis::WkbType::CircularString && type != Qgis::WkbType::LineString && type != Qgis::WkbType::NurbsCurve ) || type == mStringType ) return; mStringType = type; - if ( type == Qgis::WkbType::LineString && mPoints.count() == 3 ) + if ( type == Qgis::WkbType::LineString && mPoints.size() == 3 ) { mPoints.removeAt( 1 ); } - setVertexDrawingEnabled( type == Qgis::WkbType::CircularString ); + setVertexDrawingEnabled( type == Qgis::WkbType::CircularString || type == Qgis::WkbType::NurbsCurve ); updateCurve(); } @@ -142,8 +165,12 @@ QgsPoint QgsMapToolCaptureRubberBand::pointFromEnd( int posFromEnd ) const void QgsMapToolCaptureRubberBand::removeLastPoint() { - if ( mPoints.count() > 1 ) + if ( mPoints.size() > 1 ) + { mPoints.removeLast(); + if ( !mWeights.isEmpty() ) + mWeights.removeLast(); + } updateCurve(); } @@ -164,6 +191,9 @@ void QgsMapToolCaptureRubberBand::updateCurve() case Qgis::WkbType::CircularString: curve.reset( createCircularString() ); break; + case Qgis::WkbType::NurbsCurve: + curve.reset( createNurbsCurve() ); + break; default: return; break; @@ -171,7 +201,7 @@ void QgsMapToolCaptureRubberBand::updateCurve() if ( geometryType() == Qgis::GeometryType::Polygon ) { - std::unique_ptr geom( new QgsCurvePolygon ); + auto geom = std::make_unique(); geom->setExteriorRing( curve.release() ); setGeometry( geom.release() ); } @@ -179,11 +209,14 @@ void QgsMapToolCaptureRubberBand::updateCurve() { setGeometry( curve.release() ); } + + // Update control polygon for NURBS visualization + updateControlPolygon(); } QgsCurve *QgsMapToolCaptureRubberBand::createLinearString() { - std::unique_ptr curve( new QgsLineString ); + auto curve = std::make_unique(); if ( geometryType() == Qgis::GeometryType::Polygon ) { QgsPointSequence points = mPoints; @@ -198,12 +231,12 @@ QgsCurve *QgsMapToolCaptureRubberBand::createLinearString() QgsCurve *QgsMapToolCaptureRubberBand::createCircularString() { - std::unique_ptr curve( new QgsCircularString ); + auto curve = std::make_unique(); curve->setPoints( mPoints ); if ( geometryType() == Qgis::GeometryType::Polygon ) { // add a linear string to close the polygon - std::unique_ptr polygonCurve( new QgsCompoundCurve ); + auto polygonCurve = std::make_unique(); polygonCurve->addVertex( mFirstPolygonPoint ); if ( !mPoints.empty() ) polygonCurve->addVertex( mPoints.first() ); @@ -214,4 +247,131 @@ QgsCurve *QgsMapToolCaptureRubberBand::createCircularString() return curve.release(); } +QgsCurve *QgsMapToolCaptureRubberBand::createNurbsCurve() +{ + // Use control points from mPoints + QgsPointSequence controlPoints = mPoints; + if ( geometryType() == Qgis::GeometryType::Polygon ) + { + controlPoints.prepend( mFirstPolygonPoint ); + // For closed curves, the last control point must equal the first + controlPoints.append( mFirstPolygonPoint ); + } + + // Get degree from settings + int degree = QgsSettingsRegistryCore::settingsDigitizingNurbsDegree->value(); + const int n = controlPoints.size(); + + // Adapt degree if not enough control points + if ( n < degree + 1 ) + { + // Try lower degrees, minimum is 1 (linear) + degree = std::max( 1, n - 1 ); + if ( n < 2 ) + { + return new QgsLineString( controlPoints ); + } + } + + // Generate uniform clamped knot vector + // Size = n + degree + 1 + const int knotCount = n + degree + 1; + QVector knots( knotCount ); + + // First (degree + 1) knots are 0 + for ( int i = 0; i <= degree; ++i ) + knots[i] = 0.0; + + // Last (degree + 1) knots are 1 + for ( int i = knotCount - degree - 1; i < knotCount; ++i ) + knots[i] = 1.0; + + // Middle knots are uniformly spaced + const int numMiddleKnots = n - degree - 1; + for ( int i = 0; i < numMiddleKnots; ++i ) + { + knots[degree + 1 + i] = static_cast( i + 1 ) / ( numMiddleKnots + 1 ); + } + + // Use stored weights, or default to 1.0 + QVector weights; + if ( geometryType() == Qgis::GeometryType::Polygon ) + { + // For polygon, prepend weight 1.0 for mFirstPolygonPoint + weights.append( 1.0 ); + weights.append( mWeights ); + // Closing point weight should match the first point weight + weights.append( 1.0 ); + } + else + { + weights = mWeights; + } + // Ensure we have the right number of weights + while ( weights.size() < n ) + weights.append( 1.0 ); + weights.resize( n ); + + auto curve = std::make_unique( controlPoints, degree, knots, weights ); + + if ( geometryType() == Qgis::GeometryType::Polygon ) + { + // For polygons, we need to close the curve + auto polygonCurve = std::make_unique(); + polygonCurve->addCurve( curve.release() ); + return polygonCurve.release(); + } + else + return curve.release(); +} + +void QgsMapToolCaptureRubberBand::updateControlPolygon() +{ + if ( !mControlPolygonRubberBand ) + return; + + // Only show control polygon for NURBS curves + if ( mStringType != Qgis::WkbType::NurbsCurve || mPoints.size() < 2 ) + { + mControlPolygonRubberBand->reset( Qgis::GeometryType::Line ); + mControlPolygonRubberBand->setVisible( false ); + return; + } + + mControlPolygonRubberBand->reset( Qgis::GeometryType::Line ); + + // Add control points to form the control polygon + QgsPointSequence controlPoints = mPoints; + if ( geometryType() == Qgis::GeometryType::Polygon && !mFirstPolygonPoint.isEmpty() ) + { + controlPoints.prepend( mFirstPolygonPoint ); + } + + for ( const QgsPoint &pt : std::as_const( controlPoints ) ) + { + mControlPolygonRubberBand->addPoint( QgsPointXY( pt ) ); + } + + mControlPolygonRubberBand->setVisible( true ); +} + +double QgsMapToolCaptureRubberBand::weight( int index ) const +{ + if ( index < 0 || index >= mWeights.size() ) + return 1.0; + return mWeights[index]; +} + +bool QgsMapToolCaptureRubberBand::setWeight( int index, double weight ) +{ + if ( index < 0 || index >= mWeights.size() ) + return false; + if ( weight <= 0.0 ) + return false; + + mWeights[index] = weight; + updateCurve(); + return true; +} + ///@endcond PRIVATE diff --git a/src/gui/maptools/qgsmaptoolcapturerubberband.h b/src/gui/maptools/qgsmaptoolcapturerubberband.h index 98904c54a099..ea563b801553 100644 --- a/src/gui/maptools/qgsmaptoolcapturerubberband.h +++ b/src/gui/maptools/qgsmaptoolcapturerubberband.h @@ -17,9 +17,12 @@ #define QGSMAPTOOLCAPTURERUBBERBAND_H +#include + #include "qgsgeometryrubberband.h" class QgsMapToolCaptureRubberBand; +class QgsRubberBand; #define SIP_NO_FILE @@ -37,6 +40,9 @@ class GUI_EXPORT QgsMapToolCaptureRubberBand : public QgsGeometryRubberBand //! Constructor QgsMapToolCaptureRubberBand( QgsMapCanvas *mapCanvas, Qgis::GeometryType geomType = Qgis::GeometryType::Line ); + //! Destructor + ~QgsMapToolCaptureRubberBand() override; + /** * Returns the curve defined by the rubber band, or NULLPTR if no curve is defined. * @@ -70,7 +76,7 @@ class GUI_EXPORT QgsMapToolCaptureRubberBand : public QgsGeometryRubberBand void movePoint( int index, const QgsPoint &point ); //! Returns the points count in the rubber band (except the first point if polygon) - int pointsCount(); + int pointsCount() const; //! Returns the type of the curve (linear string or circular string) Qgis::WkbType stringType() const; @@ -87,6 +93,27 @@ class GUI_EXPORT QgsMapToolCaptureRubberBand : public QgsGeometryRubberBand //! Removes the last point of the rrubber band void removeLastPoint(); + /** + * Returns the weight at the specified control point index. + * Returns 1.0 if index is out of range. + * \since QGIS 4.0 + */ + double weight( int index ) const; + + /** + * Sets the weight at the specified control point index. + * Weight must be positive (> 0). + * \returns true if successful. + * \since QGIS 4.0 + */ + bool setWeight( int index, double weight ); + + /** + * Returns all weights. + * \since QGIS 4.0 + */ + const QVector &weights() const { return mWeights; } + private: Qgis::WkbType mStringType = Qgis::WkbType::LineString; @@ -95,9 +122,14 @@ class GUI_EXPORT QgsMapToolCaptureRubberBand : public QgsGeometryRubberBand QgsCurve *createLinearString(); QgsCurve *createCircularString(); + QgsCurve *createNurbsCurve(); QgsPointSequence mPoints; + QVector mWeights; //!< Weights for NURBS control points QgsPoint mFirstPolygonPoint; + std::unique_ptr mControlPolygonRubberBand; + + void updateControlPolygon(); }; /// @endcond diff --git a/src/gui/maptools/qgsmaptooldigitizefeature.cpp b/src/gui/maptools/qgsmaptooldigitizefeature.cpp index 70e5803c6922..c2031d97d4fd 100644 --- a/src/gui/maptools/qgsmaptooldigitizefeature.cpp +++ b/src/gui/maptools/qgsmaptooldigitizefeature.cpp @@ -54,6 +54,7 @@ bool QgsMapToolDigitizeFeature::supportsTechnique( Qgis::CaptureTechnique techni case Qgis::CaptureTechnique::CircularString: case Qgis::CaptureTechnique::Streaming: case Qgis::CaptureTechnique::Shape: + case Qgis::CaptureTechnique::NurbsCurve: return mode() != QgsMapToolCapture::CapturePoint; } return false; diff --git a/src/gui/maptools/qgsmaptooledit.cpp b/src/gui/maptools/qgsmaptooledit.cpp index 78bf58fcd857..4f0b8810d45b 100644 --- a/src/gui/maptools/qgsmaptooledit.cpp +++ b/src/gui/maptools/qgsmaptooledit.cpp @@ -62,6 +62,15 @@ QColor QgsMapToolEdit::digitizingFillColor() return QgsSettingsRegistryCore::settingsDigitizingFillColor->value(); } +void QgsMapToolEdit::applyControlPolygonStyle( QgsRubberBand *rubberBand ) +{ + if ( !rubberBand ) + return; + + rubberBand->setColor( QgsSettingsRegistryCore::settingsDigitizingControlPolygonColor->value() ); + rubberBand->setWidth( QgsSettingsRegistryCore::settingsDigitizingControlPolygonWidth->value() ); + rubberBand->setLineStyle( Qt::DashLine ); +} QgsRubberBand *QgsMapToolEdit::createRubberBand( Qgis::GeometryType geometryType, bool alternativeBand ) { diff --git a/src/gui/maptools/qgsmaptooledit.h b/src/gui/maptools/qgsmaptooledit.h index 3c9c9fea901e..6728121cc1f3 100644 --- a/src/gui/maptools/qgsmaptooledit.h +++ b/src/gui/maptools/qgsmaptooledit.h @@ -61,6 +61,14 @@ class GUI_EXPORT QgsMapToolEdit : public QgsMapTool */ QgsGeometryRubberBand *createGeometryRubberBand( Qgis::GeometryType geometryType = Qgis::GeometryType::Line, bool alternativeBand = false ) const SIP_FACTORY; + /** + * 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 + * \since QGIS 4.0 + */ + static void applyControlPolygonStyle( QgsRubberBand *rubberBand ); + private slots: //! Vector layers' editingStopped SIGNAL will eventually trigger a clean void connectLayers( const QList &layers ); diff --git a/src/gui/qgsnewmemorylayerdialog.cpp b/src/gui/qgsnewmemorylayerdialog.cpp index af2f32b3a223..98997a0102e5 100644 --- a/src/gui/qgsnewmemorylayerdialog.cpp +++ b/src/gui/qgsnewmemorylayerdialog.cpp @@ -64,6 +64,7 @@ QgsNewMemoryLayerDialog::QgsNewMemoryLayerDialog( QWidget *parent, Qt::WindowFla Qgis::WkbType::Point, Qgis::WkbType::LineString, Qgis::WkbType::CompoundCurve, + Qgis::WkbType::NurbsCurve, Qgis::WkbType::Polygon, Qgis::WkbType::CurvePolygon, Qgis::WkbType::MultiPoint, diff --git a/src/gui/qgsnewvectortabledialog.cpp b/src/gui/qgsnewvectortabledialog.cpp index 2ef82cfdde7f..5d7dba0f070c 100644 --- a/src/gui/qgsnewvectortabledialog.cpp +++ b/src/gui/qgsnewvectortabledialog.cpp @@ -161,6 +161,7 @@ QgsNewVectorTableDialog::QgsNewVectorTableDialog( QgsAbstractDatabaseProviderCon if ( conn->geometryColumnCapabilities().testFlag( QgsAbstractDatabaseProviderConnection::GeometryColumnCapability::Curves ) ) { addGeomItem( Qgis::WkbType::CompoundCurve ); + addGeomItem( Qgis::WkbType::NurbsCurve ); addGeomItem( Qgis::WkbType::CurvePolygon ); addGeomItem( Qgis::WkbType::MultiCurve ); addGeomItem( Qgis::WkbType::MultiSurface ); diff --git a/src/gui/qgssnapindicator.cpp b/src/gui/qgssnapindicator.cpp index 248b7ae2da33..5993d2216e63 100644 --- a/src/gui/qgssnapindicator.cpp +++ b/src/gui/qgssnapindicator.cpp @@ -62,10 +62,10 @@ void QgsSnapIndicator::setMatch( const QgsPointLocator::Match &match ) { iconType = QgsVertexMarker::ICON_INVERTED_TRIANGLE; // line endpoint snap } - else if ( match.hasVertex() ) + else if ( match.hasVertex() || match.hasControlPoint() ) { if ( match.layer() ) - iconType = QgsVertexMarker::ICON_BOX; // vertex snap + iconType = QgsVertexMarker::ICON_BOX; // vertex or control point snap else iconType = QgsVertexMarker::ICON_X; // intersection snap } diff --git a/src/providers/oracle/qgsoracleprovider.cpp b/src/providers/oracle/qgsoracleprovider.cpp index c093ae06de36..b0631aa7f98c 100644 --- a/src/providers/oracle/qgsoracleprovider.cpp +++ b/src/providers/oracle/qgsoracleprovider.cpp @@ -2332,6 +2332,11 @@ void QgsOracleProvider::appendGeomParam( const QgsGeometry &geom, QSqlQuery &qry case Qgis::WkbType::TriangleZM: case Qgis::WkbType::Unknown: case Qgis::WkbType::NoGeometry: + // TOOD: NURBS should be supported, but not yet tested on ORACLE + case Qgis::WkbType::NurbsCurve: + case Qgis::WkbType::NurbsCurveZ: + case Qgis::WkbType::NurbsCurveM: + case Qgis::WkbType::NurbsCurveZM: g.isNull = true; break; diff --git a/src/providers/postgres/qgspostgresconn.cpp b/src/providers/postgres/qgspostgresconn.cpp index a430a3f4f494..5c1beac59b43 100644 --- a/src/providers/postgres/qgspostgresconn.cpp +++ b/src/providers/postgres/qgspostgresconn.cpp @@ -2503,6 +2503,10 @@ void QgsPostgresConn::postgisWkbType( Qgis::WkbType wkbType, QString &geometryTy geometryType = QStringLiteral( "COMPOUNDCURVE" ); break; + case Qgis::WkbType::NurbsCurve: + geometryType = QStringLiteral( "NURBSCURVE" ); + break; + case Qgis::WkbType::CurvePolygon: geometryType = QStringLiteral( "CURVEPOLYGON" ); break; @@ -2580,7 +2584,7 @@ QString QgsPostgresConn::postgisTypeFilter( QString geomCol, Qgis::WkbType wkbTy case Qgis::GeometryType::Point: return QStringLiteral( "upper(geometrytype(%1)) IN ('POINT','POINTZ','POINTM','POINTZM','MULTIPOINT','MULTIPOINTZ','MULTIPOINTM','MULTIPOINTZM')" ).arg( geomCol ); case Qgis::GeometryType::Line: - return QStringLiteral( "upper(geometrytype(%1)) IN ('LINESTRING','LINESTRINGZ','LINESTRINGM','LINESTRINGZM','CIRCULARSTRING','CIRCULARSTRINGZ','CIRCULARSTRINGM','CIRCULARSTRINGZM','COMPOUNDCURVE','COMPOUNDCURVEZ','COMPOUNDCURVEM','COMPOUNDCURVEZM','MULTILINESTRING','MULTILINESTRINGZ','MULTILINESTRINGM','MULTILINESTRINGZM','MULTICURVE','MULTICURVEZ','MULTICURVEM','MULTICURVEZM')" ).arg( geomCol ); + return QStringLiteral( "upper(geometrytype(%1)) IN ('LINESTRING','LINESTRINGZ','LINESTRINGM','LINESTRINGZM','CIRCULARSTRING','CIRCULARSTRINGZ','CIRCULARSTRINGM','CIRCULARSTRINGZM','COMPOUNDCURVE','COMPOUNDCURVEZ','COMPOUNDCURVEM','COMPOUNDCURVEZM','NURBSCURVE','NURBSCURVEZ','NURBSCURVEM','NURBSCURVEZM','MULTILINESTRING','MULTILINESTRINGZ','MULTILINESTRINGM','MULTILINESTRINGZM','MULTICURVE','MULTICURVEZ','MULTICURVEM','MULTICURVEZM')" ).arg( geomCol ); case Qgis::GeometryType::Polygon: return QStringLiteral( "upper(geometrytype(%1)) IN ('POLYGON','POLYGONZ','POLYGONM','POLYGONZM','CURVEPOLYGON','CURVEPOLYGONZ','CURVEPOLYGONM','CURVEPOLYGONZM','MULTIPOLYGON','MULTIPOLYGONZ','MULTIPOLYGONM','MULTIPOLYGONZM','MULTIPOLYGONM','MULTISURFACE','MULTISURFACEZ','MULTISURFACEM','MULTISURFACEZM','TRIANGLE','TRIANGLEZ','TRIANGLEM','TRIANGLEZM','POLYHEDRALSURFACE','POLYHEDRALSURFACEZ','POLYHEDRALSURFACEM','POLYHEDRALSURFACEZM','TIN','TINZ','TINM','TINZM')" ).arg( geomCol ); case Qgis::GeometryType::Null: diff --git a/src/ui/qgisapp.ui b/src/ui/qgisapp.ui index 7911b8270f92..aae39447a00e 100644 --- a/src/ui/qgisapp.ui +++ b/src/ui/qgisapp.ui @@ -3371,6 +3371,21 @@ Shows placeholders for labels which could not be placed, e.g. due to overlaps wi Digitize Shape + + + true + + + + :/images/themes/default/mActionDigitizeWithNURBS.svg:/images/themes/default/mActionDigitizeWithNURBS.svg + + + Digitize with NURBS + + + Digitizes NURBS curves with control points + + diff --git a/tests/src/app/CMakeLists.txt b/tests/src/app/CMakeLists.txt index 69554ba85a27..f8a0cb1fed11 100644 --- a/tests/src/app/CMakeLists.txt +++ b/tests/src/app/CMakeLists.txt @@ -39,6 +39,7 @@ set(TESTS testqgsmaptoolmovefeature.cpp testqgsmaptoolellipse.cpp testqgsmaptoolcircle.cpp + testqgsmaptoolnurbs.cpp testqgsmaptoolrectangle.cpp testqgsmaptoolregularpolygon.cpp testqgsmaptoolsplitparts.cpp diff --git a/tests/src/app/testqgsmaptoolnurbs.cpp b/tests/src/app/testqgsmaptoolnurbs.cpp new file mode 100644 index 000000000000..b4dd30429f48 --- /dev/null +++ b/tests/src/app/testqgsmaptoolnurbs.cpp @@ -0,0 +1,195 @@ +/*************************************************************************** + testqgsmaptoolnurbs.cpp + ----------------------- + Date : 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 + +#include "qgisapp.h" +#include "qgsgeometry.h" +#include "qgsmapcanvas.h" +#include "qgsmaptooladdfeature.h" +#include "qgsmaptoolcapture.h" +#include "qgsnurbscurve.h" +#include "qgssettingsentryenumflag.h" +#include "qgssettingsregistrycore.h" +#include "qgstest.h" +#include "qgsvectorlayer.h" +#include "testqgsmaptoolutils.h" + +class TestQgsMapToolNurbs : public QObject +{ + Q_OBJECT + + public: + TestQgsMapToolNurbs() = default; + + private slots: + void initTestCase(); + void cleanupTestCase(); + + void testNurbsControlPointsMode(); + void testNurbsPolyBezierMode(); + void testNurbsControlPointsNotEnoughPoints(); + void testNurbsPolyBezierNotEnoughPoints(); + + private: + QgisApp *mQgisApp = nullptr; + QgsMapToolCapture *mMapTool = nullptr; + QgsMapCanvas *mCanvas = nullptr; + std::unique_ptr mLineLayer; + + void resetMapTool( Qgis::NurbsMode mode ); +}; + +void TestQgsMapToolNurbs::initTestCase() +{ + QgsApplication::init(); + QgsApplication::initQgis(); + + mQgisApp = new QgisApp(); + mCanvas = new QgsMapCanvas(); + mCanvas->setDestinationCrs( QgsCoordinateReferenceSystem( QStringLiteral( "EPSG:27700" ) ) ); + + mLineLayer = std::make_unique( QStringLiteral( "LineString?crs=EPSG:27700" ), QStringLiteral( "layer line" ), QStringLiteral( "memory" ) ); + QVERIFY( mLineLayer->isValid() ); + + QgsProject::instance()->addMapLayers( { mLineLayer.get() } ); + mCanvas->setLayers( { mLineLayer.get() } ); + mCanvas->setCurrentLayer( mLineLayer.get() ); + + mMapTool = new QgsMapToolAddFeature( mCanvas, QgisApp::instance()->cadDockWidget(), QgsMapToolCapture::CaptureLine ); + mCanvas->setMapTool( mMapTool ); +} + +void TestQgsMapToolNurbs::cleanupTestCase() +{ + delete mMapTool; + mLineLayer.reset(); + + QgsApplication::exitQgis(); +} + +void TestQgsMapToolNurbs::resetMapTool( Qgis::NurbsMode mode ) +{ + mMapTool->clean(); + mMapTool->setCurrentCaptureTechnique( Qgis::CaptureTechnique::NurbsCurve ); + QgsSettingsRegistryCore::settingsDigitizingNurbsMode->setValue( mode ); + QgsSettingsRegistryCore::settingsDigitizingNurbsDegree->setValue( 3 ); +} + +void TestQgsMapToolNurbs::testNurbsControlPointsMode() +{ + mLineLayer->startEditing(); + mLineLayer->dataProvider()->truncate(); + + resetMapTool( Qgis::NurbsMode::ControlPoints ); + + TestQgsMapToolAdvancedDigitizingUtils utils( mMapTool ); + + utils.mouseClick( 0, 0, Qt::LeftButton ); + utils.mouseClick( 5, 10, Qt::LeftButton ); + utils.mouseClick( 10, 5, Qt::LeftButton ); + utils.mouseClick( 15, 10, Qt::LeftButton ); + utils.mouseMove( 20, 0 ); + utils.mouseClick( 20, 0, Qt::RightButton ); + + const QgsFeatureId newFid = utils.newFeatureId(); + const QgsFeature f = mLineLayer->getFeature( newFid ); + + QVERIFY( f.isValid() ); + QVERIFY( !f.geometry().isNull() ); + + const QgsAbstractGeometry *geom = f.geometry().constGet(); + QVERIFY( geom != nullptr ); + + QCOMPARE( mLineLayer->featureCount(), 1LL ); + + mLineLayer->rollBack(); +} + +void TestQgsMapToolNurbs::testNurbsPolyBezierMode() +{ + mLineLayer->startEditing(); + mLineLayer->dataProvider()->truncate(); + + resetMapTool( Qgis::NurbsMode::PolyBezier ); + + TestQgsMapToolAdvancedDigitizingUtils utils( mMapTool ); + + utils.mouseClick( 0, 0, Qt::LeftButton ); + utils.mouseMove( 5, 5 ); + utils.mouseClick( 10, 0, Qt::LeftButton ); + utils.mouseMove( 15, -5 ); + utils.mouseClick( 20, 0, Qt::RightButton ); + + const QgsFeatureId newFid = utils.newFeatureId(); + const QgsFeature f = mLineLayer->getFeature( newFid ); + + QVERIFY( f.isValid() ); + QVERIFY( !f.geometry().isNull() ); + + const QgsAbstractGeometry *geom = f.geometry().constGet(); + QVERIFY( geom != nullptr ); + + QCOMPARE( mLineLayer->featureCount(), 1LL ); + + mLineLayer->rollBack(); +} + +void TestQgsMapToolNurbs::testNurbsControlPointsNotEnoughPoints() +{ + mLineLayer->startEditing(); + mLineLayer->dataProvider()->truncate(); + const long long count = mLineLayer->featureCount(); + + resetMapTool( Qgis::NurbsMode::ControlPoints ); + + TestQgsMapToolAdvancedDigitizingUtils utils( mMapTool ); + + utils.mouseClick( 0, 0, Qt::RightButton ); + QCOMPARE( mLineLayer->featureCount(), count ); + + utils.keyClick( Qt::Key_Escape ); + + utils.mouseClick( 0, 0, Qt::LeftButton ); + utils.mouseClick( 0, 0, Qt::RightButton ); + QCOMPARE( mLineLayer->featureCount(), count ); + + mLineLayer->rollBack(); +} + +void TestQgsMapToolNurbs::testNurbsPolyBezierNotEnoughPoints() +{ + mLineLayer->startEditing(); + mLineLayer->dataProvider()->truncate(); + const long long count = mLineLayer->featureCount(); + + resetMapTool( Qgis::NurbsMode::PolyBezier ); + + TestQgsMapToolAdvancedDigitizingUtils utils( mMapTool ); + + utils.mouseClick( 0, 0, Qt::RightButton ); + QCOMPARE( mLineLayer->featureCount(), count ); + + utils.keyClick( Qt::Key_Escape ); + + utils.mouseClick( 0, 0, Qt::LeftButton ); + utils.mouseClick( 0, 0, Qt::RightButton ); + QCOMPARE( mLineLayer->featureCount(), count ); + + mLineLayer->rollBack(); +} + +QGSTEST_MAIN( TestQgsMapToolNurbs ) +#include "testqgsmaptoolnurbs.moc" diff --git a/tests/src/app/testqgsvertexeditor.cpp b/tests/src/app/testqgsvertexeditor.cpp index f9d25268c203..32741c861c84 100644 --- a/tests/src/app/testqgsvertexeditor.cpp +++ b/tests/src/app/testqgsvertexeditor.cpp @@ -17,7 +17,9 @@ #include "qgisapp.h" #include "qgsapplication.h" +#include "qgscompoundcurve.h" #include "qgsmapcanvas.h" +#include "qgsnurbscurve.h" #include "qgstest.h" #include "qgsvectorlayer.h" #include "vertextool/qgslockedfeature.h" @@ -42,6 +44,9 @@ class TestQgsVertexEditor : public QgsTest void testColumnZMR_data(); void testColumnZMR(); + void testNurbsWeightColumn(); + void testPolyBezierRecognition(); + private: std::unique_ptr mCanvas; QgisApp *mQgisApp = nullptr; @@ -49,6 +54,8 @@ class TestQgsVertexEditor : public QgsTest std::unique_ptr mLayerLineZ; std::unique_ptr mLayerLineM; std::unique_ptr mLayerLineZM; + std::unique_ptr mLayerNurbs; + std::unique_ptr mLayerPolyBezier; std::unique_ptr mVertexEditor; }; @@ -101,6 +108,47 @@ void TestQgsVertexEditor::initTestCase() line.setGeometry( QgsGeometry::fromWkt( "LineStringZM (5 5 1, 6 6 1, 7 5 1)" ) ); mLayerLineZM->dataProvider()->addFeature( line ); QCOMPARE( mLayerLineZM->featureCount(), 1 ); + + mLayerNurbs = std::make_unique( QStringLiteral( "CompoundCurve?crs=EPSG:27700" ), QStringLiteral( "layer nurbs" ), QStringLiteral( "memory" ) ); + QVERIFY( mLayerNurbs->isValid() ); + + auto nurbs = std::make_unique( + QVector { QgsPoint( 0, 0 ), QgsPoint( 5, 10 ), QgsPoint( 10, 5 ), QgsPoint( 15, 10 ) }, + 3, + QVector { 0, 0, 0, 0, 1, 1, 1, 1 }, + QVector { 1.0, 2.0, 1.5, 1.0 } + ); + auto cc = std::make_unique(); + cc->addCurve( nurbs.release() ); + QgsFeature nurbsFeature; + nurbsFeature.setGeometry( QgsGeometry( cc.release() ) ); + mLayerNurbs->dataProvider()->addFeature( nurbsFeature ); + QCOMPARE( mLayerNurbs->featureCount(), 1 ); + + // Add a poly-Bézier with 2 segments (7 control points) for testing Alt+drag on middle anchor + mLayerPolyBezier = std::make_unique( QStringLiteral( "NurbsCurve?crs=EPSG:27700" ), QStringLiteral( "layer poly-bezier" ), QStringLiteral( "memory" ) ); + QVERIFY( mLayerPolyBezier->isValid() ); + + // Poly-Bézier: 2 cubic segments joined at anchor point + // Points: anchor0 - handle0_right - handle1_left - anchor1 - handle1_right - handle2_left - anchor2 + auto polyBezier = std::make_unique( + QVector { + QgsPoint( 0, 0 ), // anchor 0 (index 0) + QgsPoint( 2, 5 ), // handle 0 right (index 1) + QgsPoint( 4, 5 ), // handle 1 left (index 2) + QgsPoint( 5, 0 ), // anchor 1 (index 3) - middle anchor + QgsPoint( 6, -5 ), // handle 1 right (index 4) + QgsPoint( 8, -5 ), // handle 2 left (index 5) + QgsPoint( 10, 0 ) // anchor 2 (index 6) + }, + 3, + QVector { 0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2 }, // Poly-Bézier knots + QVector { 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0 } + ); + QgsFeature polyBezierFeature; + polyBezierFeature.setGeometry( QgsGeometry( polyBezier.release() ) ); + mLayerPolyBezier->dataProvider()->addFeature( polyBezierFeature ); + QCOMPARE( mLayerPolyBezier->featureCount(), 1 ); } void TestQgsVertexEditor::testColumnZMR_data() @@ -108,10 +156,10 @@ void TestQgsVertexEditor::testColumnZMR_data() QTest::addColumn( "layer" ); QTest::addColumn( "headers" ); - QTest::newRow( "Line" ) << mLayerLine.get() << ( QStringList() << QStringLiteral( "x" ) << QStringLiteral( "y" ) << QStringLiteral( "r" ) ); - QTest::newRow( "LineZ" ) << mLayerLineZ.get() << ( QStringList() << QStringLiteral( "x" ) << QStringLiteral( "y" ) << QStringLiteral( "z" ) << QStringLiteral( "r" ) ); - QTest::newRow( "LineM" ) << mLayerLineM.get() << ( QStringList() << QStringLiteral( "x" ) << QStringLiteral( "y" ) << QStringLiteral( "m" ) << QStringLiteral( "r" ) ); - QTest::newRow( "LineZM" ) << mLayerLineZM.get() << ( QStringList() << QStringLiteral( "x" ) << QStringLiteral( "y" ) << QStringLiteral( "z" ) << QStringLiteral( "m" ) << QStringLiteral( "r" ) ); + QTest::newRow( "Line" ) << mLayerLine.get() << ( QStringList() << QStringLiteral( "x" ) << QStringLiteral( "y" ) ); + QTest::newRow( "LineZ" ) << mLayerLineZ.get() << ( QStringList() << QStringLiteral( "x" ) << QStringLiteral( "y" ) << QStringLiteral( "z" ) ); + QTest::newRow( "LineM" ) << mLayerLineM.get() << ( QStringList() << QStringLiteral( "x" ) << QStringLiteral( "y" ) << QStringLiteral( "m" ) ); + QTest::newRow( "LineZM" ) << mLayerLineZM.get() << ( QStringList() << QStringLiteral( "x" ) << QStringLiteral( "y" ) << QStringLiteral( "z" ) << QStringLiteral( "m" ) ); } void TestQgsVertexEditor::testColumnZMR() @@ -131,6 +179,77 @@ void TestQgsVertexEditor::testColumnZMR() QCOMPARE( headers, hdrs ); } +void TestQgsVertexEditor::testNurbsWeightColumn() +{ + QgsLockedFeature feat( 1, mLayerNurbs.get(), mCanvas.get() ); + feat.selectVertex( 0 ); + + mVertexEditor->updateEditor( &feat ); + + QStringList hdrs; + for ( int i = 0; i < mVertexEditor->mVertexModel->columnCount(); i++ ) + hdrs << mVertexEditor->mVertexModel->headerData( i, Qt::Horizontal, Qt::DisplayRole ).toString(); + + QVERIFY( hdrs.contains( QStringLiteral( "w" ) ) ); + + QCOMPARE( mVertexEditor->mVertexModel->rowCount(), 4 ); + + const int wCol = hdrs.indexOf( QLatin1Char( 'w' ) ); + QVERIFY( wCol >= 0 ); + + const QModelIndex idx0 = mVertexEditor->mVertexModel->index( 0, wCol ); + const QModelIndex idx1 = mVertexEditor->mVertexModel->index( 1, wCol ); + const QModelIndex idx2 = mVertexEditor->mVertexModel->index( 2, wCol ); + const QModelIndex idx3 = mVertexEditor->mVertexModel->index( 3, wCol ); + + QCOMPARE( mVertexEditor->mVertexModel->data( idx0, Qt::DisplayRole ).toDouble(), 1.0 ); + QCOMPARE( mVertexEditor->mVertexModel->data( idx1, Qt::DisplayRole ).toDouble(), 2.0 ); + QCOMPARE( mVertexEditor->mVertexModel->data( idx2, Qt::DisplayRole ).toDouble(), 1.5 ); + QCOMPARE( mVertexEditor->mVertexModel->data( idx3, Qt::DisplayRole ).toDouble(), 1.0 ); +} + +void TestQgsVertexEditor::testPolyBezierRecognition() +{ + // Verify that the poly-Bézier is correctly recognized + QgsFeature f = mLayerPolyBezier->getFeature( 1 ); + QVERIFY( f.isValid() ); + + const QgsGeometry geom = f.geometry(); + QVERIFY( !geom.isNull() ); + + const QgsAbstractGeometry *abstractGeom = geom.constGet(); + QVERIFY( abstractGeom ); + + const QgsNurbsCurve *nurbs = qgsgeometry_cast( abstractGeom ); + QVERIFY( nurbs ); + QCOMPARE( nurbs->numPoints(), 7 ); + QCOMPARE( nurbs->degree(), 3 ); + QVERIFY( nurbs->isPolyBezier() ); + + // Verify that anchor indices are correct (0, 3, 6 for 2-segment poly-Bézier) + // and handles are at 1, 2, 4, 5 + const QVector &ctrlPts = nurbs->controlPoints(); + QCOMPARE( ctrlPts.size(), 7 ); + + // Anchor 0 + QCOMPARE( ctrlPts[0].x(), 0.0 ); + QCOMPARE( ctrlPts[0].y(), 0.0 ); + + // Anchor 1 (middle) + QCOMPARE( ctrlPts[3].x(), 5.0 ); + QCOMPARE( ctrlPts[3].y(), 0.0 ); + + // Anchor 2 + QCOMPARE( ctrlPts[6].x(), 10.0 ); + QCOMPARE( ctrlPts[6].y(), 0.0 ); + + // Verify vertex editor displays 7 rows for 7 control points + QgsLockedFeature feat( 1, mLayerPolyBezier.get(), mCanvas.get() ); + feat.selectVertex( 0 ); + mVertexEditor->updateEditor( &feat ); + QCOMPARE( mVertexEditor->mVertexModel->rowCount(), 7 ); +} + //runs after all tests void TestQgsVertexEditor::cleanupTestCase() { diff --git a/tests/src/core/geometry/CMakeLists.txt b/tests/src/core/geometry/CMakeLists.txt index 88be357f5538..a7dda5a07ffe 100644 --- a/tests/src/core/geometry/CMakeLists.txt +++ b/tests/src/core/geometry/CMakeLists.txt @@ -25,6 +25,7 @@ set(TESTS testqgsmultipoint.cpp testqgsmultipolygon.cpp testqgsmultisurface.cpp + testqgsnurbscurve.cpp testqgspoint.cpp testqgspointxy.cpp testqgspolygon.cpp diff --git a/tests/src/core/geometry/testqgsnurbscurve.cpp b/tests/src/core/geometry/testqgsnurbscurve.cpp new file mode 100644 index 000000000000..c309b815ee31 --- /dev/null +++ b/tests/src/core/geometry/testqgsnurbscurve.cpp @@ -0,0 +1,711 @@ +/*************************************************************************** + testqgsnurbscurve.cpp + -------------------------------------- + Date : November 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 + +#include "qgscurve.h" +#include "qgsgeometry.h" +#include "qgslinestring.h" +#include "qgsnurbscurve.h" +#include "qgspoint.h" +#include "qgstest.h" +#include "qgsvertexid.h" +#include "testgeometryutils.h" + +#include +#include + +class TestQgsNurbsCurve : public QObject +{ + Q_OBJECT + private slots: + void emptyConstructor(); + void constructorWithParams(); + void constructorDegree1(); + void constructorDegree2(); + void constructorDegree3(); + void constructorDegree4(); + void constructorDegree5(); + void clone(); + void geometryType(); + void dimension(); + void properties(); + void isBSpline(); + void isRational(); + void isBezier(); + void evaluation(); + void startEndPoints(); + void numPoints(); + void isEmpty(); + void length(); + void hasCurvedSegments(); + void curveToLine(); + void reversed(); + void boundingBox(); + void equals(); + void vertexAt(); + void addZValue(); + void addMValue(); + void dropZValue(); + void dropMValue(); + void toFromWkt(); + void toFromWktZ(); + void toFromWktM(); + void toFromWktZM(); + void toFromWktWithWeights(); + void toFromWktWithKnots(); + void toFromWktEmpty(); + void toFromWkb(); + void wkbCompatibilityWithSFCGAL(); + void asGeometry(); + void cast(); +}; + +void TestQgsNurbsCurve::emptyConstructor() +{ + QgsNurbsCurve curve; + + QVERIFY( curve.isEmpty() ); + QCOMPARE( curve.numPoints(), 0 ); + QCOMPARE( curve.vertexCount(), 0 ); + QCOMPARE( curve.degree(), 0 ); + QVERIFY( curve.controlPoints().isEmpty() ); + QVERIFY( curve.knots().isEmpty() ); + QVERIFY( curve.weights().isEmpty() ); + QVERIFY( !curve.is3D() ); + QVERIFY( !curve.isMeasure() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurve ); + QCOMPARE( curve.wktTypeStr(), QString( "NurbsCurve" ) ); + QCOMPARE( curve.geometryType(), QString( "NurbsCurve" ) ); + QCOMPARE( curve.dimension(), 1 ); +} + +void TestQgsNurbsCurve::constructorWithParams() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ), QgsPoint( 3, 1 ) }; + int degree = 2; + QVector knots { 0, 0, 0, 1, 2, 2, 2 }; + QVector weights { 1, 1, 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QVERIFY( !curve.isEmpty() ); + QCOMPARE( curve.degree(), 2 ); + QCOMPARE( curve.controlPoints().size(), 4 ); + QCOMPARE( curve.knots().size(), 7 ); + QCOMPARE( curve.weights().size(), 4 ); + + QString error; + QVERIFY( curve.isValid( error, Qgis::GeometryValidityFlags() ) ); +} + +void TestQgsNurbsCurve::constructorDegree1() +{ + // Linear NURBS curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + int degree = 1; + QVector knots { 0, 0, 1, 1 }; + QVector weights { 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QCOMPARE( curve.degree(), 1 ); + QCOMPARE( curve.controlPoints().size(), 2 ); + QCOMPARE( curve.knots().size(), 4 ); +} + +void TestQgsNurbsCurve::constructorDegree2() +{ + // Quadratic NURBS curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 5, 10 ), QgsPoint( 10, 0 ) }; + int degree = 2; + QVector knots { 0, 0, 0, 1, 1, 1 }; + QVector weights { 1, 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QCOMPARE( curve.degree(), 2 ); + QCOMPARE( curve.controlPoints().size(), 3 ); + QCOMPARE( curve.knots().size(), 6 ); +} + +void TestQgsNurbsCurve::constructorDegree3() +{ + // Cubic NURBS curve (Bézier) + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 3, 10 ), QgsPoint( 7, 10 ), QgsPoint( 10, 0 ) }; + int degree = 3; + QVector knots { 0, 0, 0, 0, 1, 1, 1, 1 }; + QVector weights { 1, 1, 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QCOMPARE( curve.degree(), 3 ); + QCOMPARE( curve.controlPoints().size(), 4 ); + QCOMPARE( curve.knots().size(), 8 ); +} + +void TestQgsNurbsCurve::constructorDegree4() +{ + // Quartic NURBS curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 2, 8 ), QgsPoint( 5, 12 ), QgsPoint( 8, 8 ), QgsPoint( 10, 0 ) }; + int degree = 4; + QVector knots { 0, 0, 0, 0, 0, 1, 1, 1, 1, 1 }; + QVector weights { 1, 1, 1, 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QCOMPARE( curve.degree(), 4 ); + QCOMPARE( curve.controlPoints().size(), 5 ); + QCOMPARE( curve.knots().size(), 10 ); +} + +void TestQgsNurbsCurve::constructorDegree5() +{ + // Quintic NURBS curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 2 ), QgsPoint( 3, 4 ), QgsPoint( 5, 6 ), QgsPoint( 7, 4 ), QgsPoint( 8, 2 ), QgsPoint( 10, 0 ) }; + int degree = 5; + QVector knots { 0, 0, 0, 0, 0, 0, 0.5, 1, 1, 1, 1, 1, 1 }; + QVector weights { 1, 1, 1, 1, 1, 1, 1 }; + + QgsNurbsCurve curve( controlPoints, degree, knots, weights ); + + QCOMPARE( curve.degree(), 5 ); + QCOMPARE( curve.controlPoints().size(), 7 ); + QCOMPARE( curve.knots().size(), 13 ); +} + +void TestQgsNurbsCurve::clone() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + QgsNurbsCurve original( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + std::unique_ptr cloned( original.clone() ); + + QVERIFY( cloned != nullptr ); + QCOMPARE( cloned->geometryType(), QString( "NurbsCurve" ) ); + QVERIFY( cloned.get() != &original ); + QGSCOMPARENEAR( original.length(), cloned->length(), 0.00001 ); +} + +void TestQgsNurbsCurve::geometryType() +{ + QgsNurbsCurve curve; + QCOMPARE( curve.geometryType(), QString( "NurbsCurve" ) ); +} + +void TestQgsNurbsCurve::dimension() +{ + QgsNurbsCurve curve; + QCOMPARE( curve.dimension(), 1 ); +} + +void TestQgsNurbsCurve::properties() +{ + // Test B-spline (all weights = 1) + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + QgsNurbsCurve bspline( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + QVERIFY( bspline.isBSpline() ); + QVERIFY( !bspline.isRational() ); +} + +void TestQgsNurbsCurve::isBSpline() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + + // B-spline: all weights = 1 + QgsNurbsCurve bspline( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + QVERIFY( bspline.isBSpline() ); + + // Not a B-spline: non-uniform weights + QgsNurbsCurve rational( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 2, 1 } ); + QVERIFY( !rational.isBSpline() ); +} + +void TestQgsNurbsCurve::isRational() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + + // Non-rational: all weights = 1 + QgsNurbsCurve nonRational( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + QVERIFY( !nonRational.isRational() ); + + // Rational: non-uniform weights + QgsNurbsCurve rational( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 2, 1 } ); + QVERIFY( rational.isRational() ); +} + +void TestQgsNurbsCurve::isBezier() +{ + // Cubic Bézier curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 2 ), QgsPoint( 2, 2 ), QgsPoint( 3, 0 ) }; + QgsNurbsCurve bezier( controlPoints, 3, QVector { 0, 0, 0, 0, 1, 1, 1, 1 }, QVector { 1, 1, 1, 1 } ); + + QVERIFY( bezier.isBezier() ); +} + +void TestQgsNurbsCurve::evaluation() +{ + // Simple linear case (degree 1) + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + QgsNurbsCurve linear( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QgsPoint start = linear.evaluate( 0.0 ); + QGSCOMPARENEAR( start.x(), 0.0, 0.00001 ); + QGSCOMPARENEAR( start.y(), 0.0, 0.00001 ); + + QgsPoint mid = linear.evaluate( 0.5 ); + QGSCOMPARENEAR( mid.x(), 5.0, 0.00001 ); + QGSCOMPARENEAR( mid.y(), 5.0, 0.00001 ); + + QgsPoint end = linear.evaluate( 1.0 ); + QGSCOMPARENEAR( end.x(), 10.0, 0.00001 ); + QGSCOMPARENEAR( end.y(), 10.0, 0.00001 ); + + // Test t below 0 returns start point + QgsPoint belowZero = linear.evaluate( -0.5 ); + QGSCOMPARENEAR( belowZero.x(), 0.0, 0.00001 ); + QGSCOMPARENEAR( belowZero.y(), 0.0, 0.00001 ); + + // Test t above 1 returns end point + QgsPoint aboveOne = linear.evaluate( 1.5 ); + QGSCOMPARENEAR( aboveOne.x(), 10.0, 0.00001 ); + QGSCOMPARENEAR( aboveOne.y(), 10.0, 0.00001 ); + + // Test evaluate on empty NURBS returns empty point + QgsNurbsCurve empty; + QgsPoint emptyEval = empty.evaluate( 0.5 ); + QVERIFY( emptyEval.isEmpty() ); +} + +void TestQgsNurbsCurve::startEndPoints() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 5 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QgsPoint start = curve.startPoint(); + QgsPoint end = curve.endPoint(); + + QGSCOMPARENEAR( start.x(), 0.0, 0.00001 ); + QGSCOMPARENEAR( start.y(), 0.0, 0.00001 ); + QGSCOMPARENEAR( end.x(), 10.0, 0.00001 ); + QGSCOMPARENEAR( end.y(), 5.0, 0.00001 ); +} + +void TestQgsNurbsCurve::numPoints() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + QgsNurbsCurve curve( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + QVERIFY( curve.numPoints() > 0 ); +} + +void TestQgsNurbsCurve::isEmpty() +{ + QgsNurbsCurve empty; + QVERIFY( empty.isEmpty() ); + + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + QVERIFY( !curve.isEmpty() ); +} + +void TestQgsNurbsCurve::length() +{ + // Simple linear case: 3-4-5 triangle + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 3, 4 ) }; + QgsNurbsCurve linear( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QGSCOMPARENEAR( linear.length(), 5.0, 0.01 ); +} + +void TestQgsNurbsCurve::hasCurvedSegments() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + QgsNurbsCurve curve( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + QVERIFY( curve.hasCurvedSegments() ); +} + +void TestQgsNurbsCurve::curveToLine() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + QgsNurbsCurve curve( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + std::unique_ptr line( curve.curveToLine() ); + + QVERIFY( line != nullptr ); + QVERIFY( line->numPoints() > 2 ); + QCOMPARE( line->startPoint().x(), curve.startPoint().x() ); + QCOMPARE( line->startPoint().y(), curve.startPoint().y() ); + QCOMPARE( line->endPoint().x(), curve.endPoint().x() ); + QCOMPARE( line->endPoint().y(), curve.endPoint().y() ); +} + +void TestQgsNurbsCurve::reversed() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 5, 5 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve original( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + std::unique_ptr reversed( original.reversed() ); + + QCOMPARE( original.startPoint().x(), reversed->endPoint().x() ); + QCOMPARE( original.startPoint().y(), reversed->endPoint().y() ); + QCOMPARE( original.endPoint().x(), reversed->startPoint().x() ); + QCOMPARE( original.endPoint().y(), reversed->startPoint().y() ); +} + +void TestQgsNurbsCurve::boundingBox() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 5, 10 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve curve( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + QgsRectangle bbox = curve.boundingBox(); + + QVERIFY( bbox.xMinimum() <= 0.0 ); + QVERIFY( bbox.xMaximum() >= 10.0 ); + QVERIFY( bbox.yMinimum() <= 0.0 ); + QVERIFY( bbox.yMaximum() >= 5.0 ); +} + +void TestQgsNurbsCurve::equals() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 1, 1 ), QgsPoint( 2, 0 ) }; + QgsNurbsCurve curve1( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + QgsNurbsCurve curve2( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + QgsNurbsCurve curve3( QVector { QgsPoint( 0, 0 ), QgsPoint( 2, 2 ) }, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QVERIFY( curve1.equals( curve2 ) ); + QVERIFY( !curve1.equals( curve3 ) ); +} + +void TestQgsNurbsCurve::vertexAt() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + if ( curve.numPoints() > 0 ) + { + QgsPoint vertex = curve.vertexAt( QgsVertexId( 0, 0, 0 ) ); + QVERIFY( !vertex.isEmpty() ); + } +} + +void TestQgsNurbsCurve::addZValue() +{ + // Start with a 2D curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QVERIFY( !curve.is3D() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurve ); + + // Add Z value + QVERIFY( curve.addZValue( 5.0 ) ); + QVERIFY( curve.is3D() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurveZ ); + + // Check control points now have Z + QGSCOMPARENEAR( curve.controlPoints()[0].z(), 5.0, 0.00001 ); + QGSCOMPARENEAR( curve.controlPoints()[1].z(), 5.0, 0.00001 ); + + // Adding Z again should fail + QVERIFY( !curve.addZValue( 10.0 ) ); +} + +void TestQgsNurbsCurve::addMValue() +{ + // Start with a 2D curve + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 10 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QVERIFY( !curve.isMeasure() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurve ); + + // Add M value + QVERIFY( curve.addMValue( 100.0 ) ); + QVERIFY( curve.isMeasure() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurveM ); + + // Check control points now have M + QGSCOMPARENEAR( curve.controlPoints()[0].m(), 100.0, 0.00001 ); + QGSCOMPARENEAR( curve.controlPoints()[1].m(), 100.0, 0.00001 ); + + // Adding M again should fail + QVERIFY( !curve.addMValue( 200.0 ) ); +} + +void TestQgsNurbsCurve::dropZValue() +{ + // Start with a 3D curve + QVector controlPoints { QgsPoint( 0, 0, 5 ), QgsPoint( 10, 10, 15 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QVERIFY( curve.is3D() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurveZ ); + + // Drop Z value + QVERIFY( curve.dropZValue() ); + QVERIFY( !curve.is3D() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurve ); + + // Dropping Z again should fail + QVERIFY( !curve.dropZValue() ); +} + +void TestQgsNurbsCurve::dropMValue() +{ + // Start with an M curve + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( QStringLiteral( "NURBSCURVE M(1, (0 0 10, 10 10 20))" ) ) ); + + QVERIFY( curve.isMeasure() ); + QVERIFY( !curve.is3D() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurveM ); + + // Drop M value + QVERIFY( curve.dropMValue() ); + QVERIFY( !curve.isMeasure() ); + QCOMPARE( curve.wkbType(), Qgis::WkbType::NurbsCurve ); + + // Dropping M again should fail + QVERIFY( !curve.dropMValue() ); +} + +void TestQgsNurbsCurve::toFromWkt() +{ + // Basic NURBS curve + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( QStringLiteral( "NURBSCURVE(2, (0 0, 5 10, 10 0))" ) ) ); + + QCOMPARE( curve.degree(), 2 ); + QCOMPARE( curve.controlPoints().size(), 3 ); + QVERIFY( !curve.isEmpty() ); + + QString wkt = curve.asWkt(); + QVERIFY( wkt.contains( QStringLiteral( "NurbsCurve" ) ) ); +} + +void TestQgsNurbsCurve::toFromWktZ() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( QStringLiteral( "NURBSCURVE Z(2, (0 0 0, 5 10 5, 10 0 0))" ) ) ); + + QVERIFY( curve.is3D() ); + QCOMPARE( curve.degree(), 2 ); + + QString wkt = curve.asWkt(); + QVERIFY( wkt.contains( QStringLiteral( "NurbsCurve Z" ) ) ); +} + +void TestQgsNurbsCurve::toFromWktM() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( QStringLiteral( "NURBSCURVE M(2, (0 0 10, 5 10 20, 10 0 30))" ) ) ); + + QVERIFY( curve.isMeasure() ); + QVERIFY( !curve.is3D() ); + QCOMPARE( curve.degree(), 2 ); + + QString wkt = curve.asWkt(); + QVERIFY( wkt.contains( QStringLiteral( "NurbsCurve M" ) ) ); +} + +void TestQgsNurbsCurve::toFromWktZM() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( QStringLiteral( "NURBSCURVE ZM(2, (0 0 1 10, 5 10 2 20, 10 0 3 30))" ) ) ); + + QVERIFY( curve.is3D() ); + QVERIFY( curve.isMeasure() ); + QCOMPARE( curve.degree(), 2 ); + + QString wkt = curve.asWkt(); + QVERIFY( wkt.contains( QStringLiteral( "NurbsCurve ZM" ) ) ); +} + +void TestQgsNurbsCurve::toFromWktWithWeights() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( QStringLiteral( "NURBSCURVE(2, (0 0, 5 10, 10 0), (1, 2, 1))" ) ) ); + + QCOMPARE( curve.degree(), 2 ); + QCOMPARE( curve.controlPoints().size(), 3 ); + QVERIFY( curve.isRational() ); + QCOMPARE( curve.weights().size(), 3 ); +} + +void TestQgsNurbsCurve::toFromWktWithKnots() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( QStringLiteral( "NURBSCURVE(2, (0 0, 3 6, 6 3, 9 0), (1, 1, 1, 1), (0, 0, 0, 0.5, 1, 1, 1))" ) ) ); + + QCOMPARE( curve.degree(), 2 ); + QCOMPARE( curve.controlPoints().size(), 4 ); + QCOMPARE( curve.knots().size(), 7 ); +} + +void TestQgsNurbsCurve::toFromWktEmpty() +{ + QgsNurbsCurve curve; + QVERIFY( curve.fromWkt( QStringLiteral( "NURBSCURVE EMPTY" ) ) ); + + QVERIFY( curve.isEmpty() ); +} + +void TestQgsNurbsCurve::toFromWkb() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 5, 10 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve original( controlPoints, 2, QVector { 0, 0, 0, 1, 1, 1 }, QVector { 1, 1, 1 } ); + + QByteArray wkb = original.asWkb(); + QVERIFY( !wkb.isEmpty() ); + + QgsNurbsCurve restored; + QgsConstWkbPtr wkbPtr( wkb ); + restored.fromWkb( wkbPtr ); + + QCOMPARE( restored.degree(), original.degree() ); + QCOMPARE( restored.controlPoints().size(), original.controlPoints().size() ); + QVERIFY( restored.equals( original ) ); +} + +void TestQgsNurbsCurve::wkbCompatibilityWithSFCGAL() +{ + // WKB test cases compatible with SFCGAL and PostGIS + struct TestCase + { + QString wkt; + QString wkb; + }; + + QVector testCases = { + { QStringLiteral( "NURBSCURVE(1, (0 0, 10 10))" ), QStringLiteral( "011500000001000000020000000100000000000000000000000000000000000100000000000024400000000000002440000400000000000000000000000000000000000000000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(1, (0 0, 5 5, 10 0))" ), QStringLiteral( "011500000001000000030000000100000000000000000000000000000000000100000000000014400000000000001440000100000000000024400000000000000000000500000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(1, (0 0, 10 10), (1, 1))" ), QStringLiteral( "011500000001000000020000000100000000000000000000000000000000000100000000000024400000000000002440000400000000000000000000000000000000000000000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(1, (0 0, 10 10), (0.5, 2.0))" ), QStringLiteral( "01150000000100000002000000010000000000000000000000000000000001000000000000e03f01000000000000244000000000000024400100000000000000400400000000000000000000000000000000000000000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(1, (0 0, 5 5, 10 0), (1, 1.5, 1))" ), QStringLiteral( "01150000000100000003000000010000000000000000000000000000000000010000000000001440000000000000144001000000000000f83f0100000000000024400000000000000000000500000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(1, (0 0, 10 10), (1, 1), (0, 0, 1, 1))" ), QStringLiteral( "011500000001000000020000000100000000000000000000000000000000000100000000000024400000000000002440000400000000000000000000000000000000000000000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(1, (0 0, 5 5, 10 0), (1, 1, 1), (0, 0, 0.5, 1, 1))" ), QStringLiteral( "011500000001000000030000000100000000000000000000000000000000000100000000000014400000000000001440000100000000000024400000000000000000000500000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (0 0, 5 10, 10 0))" ), QStringLiteral( "0115000000020000000300000001000000000000000000000000000000000001000000000000144000000000000024400001000000000000244000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (1 0, 1 1, 0 1))" ), QStringLiteral( "0115000000020000000300000001000000000000f03f00000000000000000001000000000000f03f000000000000f03f00010000000000000000000000000000f03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (-5 -5, 0 10, 5 -5))" ), QStringLiteral( "011500000002000000030000000100000000000014c000000000000014c00001000000000000000000000000000024400001000000000000144000000000000014c00006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (0 0, 2 8, 8 2, 10 10))" ), QStringLiteral( "0115000000020000000400000001000000000000000000000000000000000001000000000000004000000000000020400001000000000000204000000000000000400001000000000000244000000000000024400007000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (0 0, 5 10, 10 0), (1, 1, 1))" ), QStringLiteral( "0115000000020000000300000001000000000000000000000000000000000001000000000000144000000000000024400001000000000000244000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (1 0, 1 1, 0 1), (1, 0.5, 1))" ), QStringLiteral( "0115000000020000000300000001000000000000f03f00000000000000000001000000000000f03f000000000000f03f01000000000000e03f010000000000000000000000000000f03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (0 0, 5 10, 10 0), (1, 3, 1))" ), QStringLiteral( "01150000000200000003000000010000000000000000000000000000000000010000000000001440000000000000244001000000000000084001000000000000244000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (0 0, 5 10, 10 0), (0.5, 1, 0.5))" ), QStringLiteral( "01150000000200000003000000010000000000000000000000000000000001000000000000e03f010000000000001440000000000000244000010000000000002440000000000000000001000000000000e03f06000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (0.5 0.25, 2.75 5.5, 5.0 0.25), (1, 1.5, 1))" ), QStringLiteral( "0115000000020000000300000001000000000000e03f000000000000d03f00010000000000000640000000000000164001000000000000f83f010000000000001440000000000000d03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (0 0, 5 10, 10 0), (1, 1, 1), (0, 0, 0, 1, 1, 1))" ), QStringLiteral( "0115000000020000000300000001000000000000000000000000000000000001000000000000144000000000000024400001000000000000244000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (1 0, 1 1, 0 1), (1, 0.5, 1), (0, 0, 0, 1, 1, 1))" ), QStringLiteral( "0115000000020000000300000001000000000000f03f00000000000000000001000000000000f03f000000000000f03f01000000000000e03f010000000000000000000000000000f03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0))" ), QStringLiteral( "01150000000300000004000000010000000000000000000000000000000000010000000000000840000000000000244000010000000000001c40000000000000244000010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (0 0, 2 8, 5 12, 8 8, 10 0))" ), QStringLiteral( "01150000000300000005000000010000000000000000000000000000000000010000000000000040000000000000204000010000000000001440000000000000284000010000000000002040000000000000204000010000000000002440000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (5 0, 10 2.5, 10 7.5, 5 10, 0 7.5, 0 2.5, 5 0))" ), QStringLiteral( "011500000003000000070000000100000000000014400000000000000000000100000000000024400000000000000440000100000000000024400000000000001e40000100000000000014400000000000002440000100000000000000000000000000001e40000100000000000000000000000000000440000100000000000014400000000000000000000b0000000000000000000000000000000000000000000000000000000000000000000000000000000000d03f000000000000e03f000000000000e83f000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (0 0, 5 5, 10 0, 15 -5, 20 0))" ), QStringLiteral( "01150000000300000005000000010000000000000000000000000000000000010000000000001440000000000000144000010000000000002440000000000000000000010000000000002e4000000000000014c000010000000000003440000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (1, 1, 1, 1))" ), QStringLiteral( "01150000000300000004000000010000000000000000000000000000000000010000000000000840000000000000244000010000000000001c40000000000000244000010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (1, 2, 2, 1))" ), QStringLiteral( "011500000003000000040000000100000000000000000000000000000000000100000000000008400000000000002440010000000000000040010000000000001c400000000000002440010000000000000040010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (0.5, 1.5, 2.0, 0.8))" ), QStringLiteral( "01150000000300000004000000010000000000000000000000000000000001000000000000e03f010000000000000840000000000000244001000000000000f83f010000000000001c4000000000000024400100000000000000400100000000000024400000000000000000019a9999999999e93f080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (0 0, 2 8, 5 12, 8 8, 10 0), (1, 3, 5, 3, 1))" ), QStringLiteral( "01150000000300000005000000010000000000000000000000000000000000010000000000000040000000000000204001000000000000084001000000000000144000000000000028400100000000000014400100000000000020400000000000002040010000000000000840010000000000002440000000000000000000090000000000000000000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (1, 1, 1, 1), (0, 0, 0, 0, 1, 1, 1, 1))" ), QStringLiteral( "01150000000300000004000000010000000000000000000000000000000000010000000000000840000000000000244000010000000000001c40000000000000244000010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (1, 2, 2, 1), (0, 0, 0, 0, 1, 1, 1, 1))" ), QStringLiteral( "011500000003000000040000000100000000000000000000000000000000000100000000000008400000000000002440010000000000000040010000000000001c400000000000002440010000000000000040010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(3, (0 0, 3 10, 7 10, 10 0), (1, 1, 1, 1), (0, 0, 0, 0, 0.3, 0.7, 1, 1))" ), QStringLiteral( "01150000000300000004000000010000000000000000000000000000000000010000000000000840000000000000244000010000000000001c40000000000000244000010000000000002440000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000333333333333d33f666666666666e63f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(4, (0 0, 2 8, 5 12, 8 8, 10 0))" ), QStringLiteral( "011500000004000000050000000100000000000000000000000000000000000100000000000000400000000000002040000100000000000014400000000000002840000100000000000020400000000000002040000100000000000024400000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(4, (0 0, 1 5, 3 8, 7 6, 10 10, 12 0))" ), QStringLiteral( "0115000000040000000600000001000000000000000000000000000000000001000000000000f03f000000000000144000010000000000000840000000000000204000010000000000001c400000000000001840000100000000000024400000000000002440000100000000000028400000000000000000000b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(4, (0 0, 2 8, 5 12, 8 8, 10 0), (1, 1.5, 2, 1.5, 1))" ), QStringLiteral( "01150000000400000005000000010000000000000000000000000000000000010000000000000040000000000000204001000000000000f83f0100000000000014400000000000002840010000000000000040010000000000002040000000000000204001000000000000f83f0100000000000024400000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(4, (0 0, 2 8, 5 12, 8 8, 10 0), (1, 1, 1, 1, 1), (0, 0, 0, 0, 0, 1, 1, 1, 1, 1))" ), QStringLiteral( "011500000004000000050000000100000000000000000000000000000000000100000000000000400000000000002040000100000000000014400000000000002840000100000000000020400000000000002040000100000000000024400000000000000000000a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(5, (0 0, 1 2, 3 4, 5 6, 7 4, 8 2, 10 0), (1, 1, 1, 1, 1, 1, 1))" ), QStringLiteral( "0115000000050000000700000001000000000000000000000000000000000001000000000000f03f000000000000004000010000000000000840000000000000104000010000000000001440000000000000184000010000000000001c400000000000001040000100000000000020400000000000000040000100000000000024400000000000000000000d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000e03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + // 3D (Z) test cases + { QStringLiteral( "NURBSCURVE Z(1, (0 0 0, 10 10 5))" ), QStringLiteral( "01fd0300000100000002000000010000000000000000000000000000000000000000000000000001000000000000244000000000000024400000000000001440000400000000000000000000000000000000000000000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE Z(2, (0 0 0, 5 10 5, 10 0 0))" ), QStringLiteral( "01fd030000020000000300000001000000000000000000000000000000000000000000000000000100000000000014400000000000002440000000000000144000010000000000002440000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE Z(2, (0 0 0, 5 10 5, 10 0 0), (1, 2, 1))" ), QStringLiteral( "01fd0300000200000003000000010000000000000000000000000000000000000000000000000001000000000000144000000000000024400000000000001440010000000000000040010000000000002440000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE Z(3, (0 0 0, 3 10 3, 7 10 7, 10 0 10))" ), QStringLiteral( "01fd030000030000000400000001000000000000000000000000000000000000000000000000000100000000000008400000000000002440000000000000084000010000000000001c4000000000000024400000000000001c40000100000000000024400000000000000000000000000000244000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE Z(3, (0 0 0, 3 10 3, 7 10 7, 10 0 10), (1, 1.5, 1.5, 1), (0, 0, 0, 0, 1, 1, 1, 1))" ), QStringLiteral( "01fd030000030000000400000001000000000000000000000000000000000000000000000000000100000000000008400000000000002440000000000000084001000000000000f83f010000000000001c4000000000000024400000000000001c4001000000000000f83f0100000000000024400000000000000000000000000000244000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + // M test cases + { QStringLiteral( "NURBSCURVE M(1, (0 0 0, 10 10 3600))" ), QStringLiteral( "01e5070000010000000200000001000000000000000000000000000000000000000000000000000100000000000024400000000000002440000000000020ac40000400000000000000000000000000000000000000000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE M(2, (0 0 0, 5 10 1800, 10 0 3600))" ), QStringLiteral( "01e50700000200000003000000010000000000000000000000000000000000000000000000000001000000000000144000000000000024400000000000209c40000100000000000024400000000000000000000000000020ac400006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE M(2, (0 0 0, 5 10 1800, 10 0 3600), (1, 2, 1))" ), QStringLiteral( "01e50700000200000003000000010000000000000000000000000000000000000000000000000001000000000000144000000000000024400000000000209c400100000000000000400100000000000024400000000000000000000000000020ac400006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + // ZM test cases + { QStringLiteral( "NURBSCURVE ZM(1, (0 0 0 0, 10 10 5 3600))" ), QStringLiteral( "01cd0b000001000000020000000100000000000000000000000000000000000000000000000000000000000000000001000000000000244000000000000024400000000000001440000000000020ac40000400000000000000000000000000000000000000000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE ZM(2, (0 0 0 0, 5 10 5 1800, 10 0 0 3600))" ), QStringLiteral( "01cd0b0000020000000300000001000000000000000000000000000000000000000000000000000000000000000000010000000000001440000000000000244000000000000014400000000000209c400001000000000000244000000000000000000000000000000000000000000020ac400006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE ZM(2, (0 0 0 0, 5 10 5 1800, 10 0 0 3600), (1, 2, 1))" ), QStringLiteral( "01cd0b0000020000000300000001000000000000000000000000000000000000000000000000000000000000000000010000000000001440000000000000244000000000000014400000000000209c4001000000000000004001000000000000244000000000000000000000000000000000000000000020ac400006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE ZM(3, (0 0 0 0, 3 10 3 1200, 7 10 7 2400, 10 0 10 3600), (1, 1.5, 1.5, 1), (0, 0, 0, 0, 1, 1, 1, 1))" ), QStringLiteral( "01cd0b0000030000000400000001000000000000000000000000000000000000000000000000000000000000000000010000000000000840000000000000244000000000000008400000000000c0924001000000000000f83f010000000000001c4000000000000024400000000000001c400000000000c0a24001000000000000f83f01000000000000244000000000000000000000000000002440000000000020ac4000080000000000000000000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f000000000000f03f" ) }, + // Additional rational curve test cases + { QStringLiteral( "NURBSCURVE(2, (1 0, 1 1, 0 1), (1, 0.5, 1), (0, 0, 0, 1, 1, 1))" ), QStringLiteral( "0115000000020000000300000001000000000000f03f00000000000000000001000000000000f03f000000000000f03f01000000000000e03f010000000000000000000000000000f03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (2 0, 2 1, 0 1), (1, 0.5, 1), (0, 0, 0, 1, 1, 1))" ), QStringLiteral( "01150000000200000003000000010000000000000040000000000000000000010000000000000040000000000000f03f01000000000000e03f010000000000000000000000000000f03f0006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (0 0, 1 1, 2 0), (1, 1, 1), (0, 0, 0, 1, 1, 1))" ), QStringLiteral( "0115000000020000000300000001000000000000000000000000000000000001000000000000f03f000000000000f03f0001000000000000004000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (0 0, 5 5, 10 0))" ), QStringLiteral( "0115000000020000000300000001000000000000000000000000000000000001000000000000144000000000000014400001000000000000244000000000000000000006000000000000000000000000000000000000000000000000000000000000000000f03f000000000000f03f000000000000f03f" ) }, + { QStringLiteral( "NURBSCURVE(2, (0 0, 1 2, 2 1, 3 3, 4 2, 5 4, 6 3, 7 5, 8 4, 9 6, 10 5))" ), QStringLiteral( "0115000000020000000b00000001000000000000000000000000000000000001000000000000f03f000000000000004000010000000000000040000000000000f03f00010000000000000840000000000000084000010000000000001040000000000000004000010000000000001440000000000000104000010000000000001840000000000000084000010000000000001c400000000000001440000100000000000020400000000000001040000100000000000022400000000000001840000100000000000024400000000000001440000e0000000000000000000000000000000000000000000000000000001cc7711cc771bc3f1cc7711cc771cc3f555555555555d53f1cc7711cc771dc3f721cc7711cc7e13f555555555555e53f398ee3388ee3e83f1cc7711cc771ec3f000000000000f03f000000000000f03f000000000000f03f" ) }, + }; + + int wktSuccessCount = 0; + int wkbSuccessCount = 0; + + for ( const TestCase &testCase : testCases ) + { + // Test 1: WKT parsing + QgsGeometry geomFromWkt = QgsGeometry::fromWkt( testCase.wkt ); + if ( !geomFromWkt.isNull() ) + { + wktSuccessCount++; + + // Test 3: Convert to WKB and compare + QByteArray wkbFromWkt = geomFromWkt.asWkb(); + QString wkbHex = QString::fromLatin1( wkbFromWkt.toHex() ); + QCOMPARE( wkbHex, testCase.wkb ); + } + + // Test 2: WKB parsing + QByteArray wkbBytes = QByteArray::fromHex( testCase.wkb.toLatin1() ); + QgsGeometry geomFromWkb; + geomFromWkb.fromWkb( wkbBytes ); + if ( !geomFromWkb.isNull() ) + { + wkbSuccessCount++; + + // Test 4: Convert to WKB and compare (round-trip) + QByteArray wkbRoundTrip = geomFromWkb.asWkb(); + QString wkbHexRoundTrip = QString::fromLatin1( wkbRoundTrip.toHex() ); + QCOMPARE( wkbHexRoundTrip, testCase.wkb ); + } + } + + // All test cases should pass + QCOMPARE( wktSuccessCount, testCases.size() ); + QCOMPARE( wkbSuccessCount, testCases.size() ); +} + +void TestQgsNurbsCurve::asGeometry() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QgsGeometry geom( curve.clone() ); + + QVERIFY( !geom.isNull() ); + QVERIFY( geom.length() > 0.0 ); +} + +void TestQgsNurbsCurve::cast() +{ + QVector controlPoints { QgsPoint( 0, 0 ), QgsPoint( 10, 0 ) }; + QgsNurbsCurve curve( controlPoints, 1, QVector { 0, 0, 1, 1 }, QVector { 1, 1 } ); + + QVERIFY( QgsNurbsCurve::cast( &curve ) ); + + QgsLineString line; + QVERIFY( !QgsNurbsCurve::cast( &line ) ); +} + +QGSTEST_MAIN( TestQgsNurbsCurve ) +#include "testqgsnurbscurve.moc" diff --git a/tests/src/gui/CMakeLists.txt b/tests/src/gui/CMakeLists.txt index 3bc50c895014..be88da1c03ec 100644 --- a/tests/src/gui/CMakeLists.txt +++ b/tests/src/gui/CMakeLists.txt @@ -24,6 +24,7 @@ set(TESTS testqgsadvanceddigitizingdockwidget.cpp testqgsadvanceddigitizingtoolsregistry.cpp testqgsannotationitemguiregistry.cpp + testqgsbezierdata.cpp testqgsmaptoolzoom.cpp testqgsmaptooledit.cpp testqgscategorizedrendererwidget.cpp diff --git a/tests/src/gui/testqgsbezierdata.cpp b/tests/src/gui/testqgsbezierdata.cpp new file mode 100644 index 000000000000..196998520523 --- /dev/null +++ b/tests/src/gui/testqgsbezierdata.cpp @@ -0,0 +1,234 @@ +/*************************************************************************** + testqgsbezierdata.cpp + -------------------------------------- + Date : December 2025 + Copyright : (C) 2025 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 + +#include "qgsapplication.h" +#include "qgsbezierdata.h" +#include "qgsnurbscurve.h" +#include "qgstest.h" + +#include + +class TestQgsBezierData : public QObject +{ + Q_OBJECT + + public: + TestQgsBezierData() = default; + + private slots: + void initTestCase(); + void cleanupTestCase(); + void init(); + void cleanup(); + + void testConstructor(); + void testAddAnchor(); + void testMoveAnchor(); + void testMoveHandle(); + void testInsertAnchor(); + void testDeleteAnchor(); + void testRetractHandle(); + void testExtendHandle(); + void testFindClosestAnchor(); + void testFindClosestHandle(); + void testInterpolate(); + void testAsNurbsCurve(); + void testClear(); +}; + +void TestQgsBezierData::initTestCase() +{ + QgsApplication::init(); + QgsApplication::initQgis(); +} + +void TestQgsBezierData::cleanupTestCase() +{ + QgsApplication::exitQgis(); +} + +void TestQgsBezierData::init() +{ +} + +void TestQgsBezierData::cleanup() +{ +} + +void TestQgsBezierData::testConstructor() +{ + QgsBezierData data; + QVERIFY( data.isEmpty() ); + QCOMPARE( data.anchorCount(), 0 ); + QCOMPARE( data.handleCount(), 0 ); +} + +void TestQgsBezierData::testAddAnchor() +{ + QgsBezierData data; + + data.addAnchor( QgsPoint( 0, 0 ) ); + QCOMPARE( data.anchorCount(), 1 ); + QCOMPARE( data.handleCount(), 2 ); + QCOMPARE( data.anchor( 0 ), QgsPoint( 0, 0 ) ); + QCOMPARE( data.handle( 0 ), QgsPoint( 0, 0 ) ); + QCOMPARE( data.handle( 1 ), QgsPoint( 0, 0 ) ); + + data.addAnchor( QgsPoint( 10, 0 ) ); + QCOMPARE( data.anchorCount(), 2 ); + QCOMPARE( data.handleCount(), 4 ); + QCOMPARE( data.anchor( 1 ), QgsPoint( 10, 0 ) ); +} + +void TestQgsBezierData::testMoveAnchor() +{ + QgsBezierData data; + data.addAnchor( QgsPoint( 0, 0 ) ); + data.moveHandle( 1, QgsPoint( 3, 3 ) ); + + data.moveAnchor( 0, QgsPoint( 5, 5 ) ); + QCOMPARE( data.anchor( 0 ), QgsPoint( 5, 5 ) ); + QCOMPARE( data.handle( 1 ), QgsPoint( 8, 8 ) ); +} + +void TestQgsBezierData::testMoveHandle() +{ + QgsBezierData data; + data.addAnchor( QgsPoint( 0, 0 ) ); + data.addAnchor( QgsPoint( 10, 0 ) ); + + data.moveHandle( 1, QgsPoint( 3, 3 ) ); + QCOMPARE( data.handle( 1 ), QgsPoint( 3, 3 ) ); + QCOMPARE( data.handle( 0 ), QgsPoint( 0, 0 ) ); +} + +void TestQgsBezierData::testInsertAnchor() +{ + QgsBezierData data; + data.addAnchor( QgsPoint( 0, 0 ) ); + data.addAnchor( QgsPoint( 20, 0 ) ); + + data.insertAnchor( 1, QgsPoint( 10, 5 ) ); + QCOMPARE( data.anchorCount(), 3 ); + QCOMPARE( data.anchor( 0 ), QgsPoint( 0, 0 ) ); + QCOMPARE( data.anchor( 1 ), QgsPoint( 10, 5 ) ); + QCOMPARE( data.anchor( 2 ), QgsPoint( 20, 0 ) ); +} + +void TestQgsBezierData::testDeleteAnchor() +{ + QgsBezierData data; + data.addAnchor( QgsPoint( 0, 0 ) ); + data.addAnchor( QgsPoint( 10, 0 ) ); + data.addAnchor( QgsPoint( 20, 0 ) ); + + data.deleteAnchor( 1 ); + QCOMPARE( data.anchorCount(), 2 ); + QCOMPARE( data.anchor( 0 ), QgsPoint( 0, 0 ) ); + QCOMPARE( data.anchor( 1 ), QgsPoint( 20, 0 ) ); +} + +void TestQgsBezierData::testRetractHandle() +{ + QgsBezierData data; + data.addAnchor( QgsPoint( 0, 0 ) ); + data.moveHandle( 1, QgsPoint( 5, 5 ) ); + QCOMPARE( data.handle( 1 ), QgsPoint( 5, 5 ) ); + + data.retractHandle( 1 ); + QCOMPARE( data.handle( 1 ), QgsPoint( 0, 0 ) ); +} + +void TestQgsBezierData::testExtendHandle() +{ + QgsBezierData data; + data.addAnchor( QgsPoint( 0, 0 ) ); + QCOMPARE( data.handle( 1 ), QgsPoint( 0, 0 ) ); + + data.extendHandle( 1, QgsPoint( 5, 5 ) ); + QCOMPARE( data.handle( 1 ), QgsPoint( 5, 5 ) ); +} + +void TestQgsBezierData::testFindClosestAnchor() +{ + QgsBezierData data; + data.addAnchor( QgsPoint( 0, 0 ) ); + data.addAnchor( QgsPoint( 10, 0 ) ); + data.addAnchor( QgsPoint( 20, 0 ) ); + + QCOMPARE( data.findClosestAnchor( QgsPoint( 0.5, 0 ), 1.0 ), 0 ); + QCOMPARE( data.findClosestAnchor( QgsPoint( 9.5, 0 ), 1.0 ), 1 ); + QCOMPARE( data.findClosestAnchor( QgsPoint( 100, 100 ), 1.0 ), -1 ); +} + +void TestQgsBezierData::testFindClosestHandle() +{ + QgsBezierData data; + data.addAnchor( QgsPoint( 0, 0 ) ); + data.moveHandle( 1, QgsPoint( 5, 5 ) ); + + QCOMPARE( data.findClosestHandle( QgsPoint( 4.5, 4.5 ), 1.0 ), 1 ); + QCOMPARE( data.findClosestHandle( QgsPoint( 100, 100 ), 1.0 ), -1 ); +} + +void TestQgsBezierData::testInterpolate() +{ + QgsBezierData data; + data.addAnchor( QgsPoint( 0, 0 ) ); + data.addAnchor( QgsPoint( 10, 10 ) ); + + QgsPointSequence points = data.interpolate(); + QVERIFY( !points.isEmpty() ); + QCOMPARE( points.first(), QgsPoint( 0, 0 ) ); + QCOMPARE( points.last(), QgsPoint( 10, 10 ) ); +} + +void TestQgsBezierData::testAsNurbsCurve() +{ + QgsBezierData data; + QVERIFY( data.asNurbsCurve() == nullptr ); + + data.addAnchor( QgsPoint( 0, 0 ) ); + QVERIFY( data.asNurbsCurve() == nullptr ); + + data.addAnchor( QgsPoint( 10, 10 ) ); + data.moveHandle( 1, QgsPoint( 3, 3 ) ); + data.moveHandle( 2, QgsPoint( 7, 7 ) ); + + std::unique_ptr nurbs( data.asNurbsCurve() ); + QVERIFY( nurbs != nullptr ); + QCOMPARE( nurbs->degree(), 3 ); + QCOMPARE( nurbs->controlPoints().size(), 4 ); + QCOMPARE( nurbs->startPoint(), QgsPoint( 0, 0 ) ); + QCOMPARE( nurbs->endPoint(), QgsPoint( 10, 10 ) ); +} + +void TestQgsBezierData::testClear() +{ + QgsBezierData data; + data.addAnchor( QgsPoint( 0, 0 ) ); + data.addAnchor( QgsPoint( 10, 0 ) ); + QVERIFY( !data.isEmpty() ); + + data.clear(); + QVERIFY( data.isEmpty() ); + QCOMPARE( data.anchorCount(), 0 ); + QCOMPARE( data.handleCount(), 0 ); +} + +QGSTEST_MAIN( TestQgsBezierData ) +#include "testqgsbezierdata.moc" diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index ba5390fc888d..96ede8c81ae6 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -232,6 +232,7 @@ ADD_PYTHON_TEST(PyQgsNominatimGeocoder test_qgsnominatimgeocoder.py) ADD_PYTHON_TEST(PyQgsNullSymbolRenderer test_qgsnullsymbolrenderer.py) ADD_PYTHON_TEST(PyQgsNumericFormat test_qgsnumericformat.py) ADD_PYTHON_TEST(PyQgsOapifProvider test_provider_oapif.py) +ADD_PYTHON_TEST(PyQgsNurbsCurve test_qgsnurbscurve.py) ADD_PYTHON_TEST(PyQgsObjectCustomProperties test_qgsobjectcustomproperties.py) ADD_PYTHON_TEST(PyQgsObjectVisitor test_qgsobjectvisitor.py) ADD_PYTHON_TEST(PyQgsOgcUtils test_qgsogcutils.py) diff --git a/tests/src/python/test_qgsnurbscurve.py b/tests/src/python/test_qgsnurbscurve.py new file mode 100644 index 000000000000..69c931f590c0 --- /dev/null +++ b/tests/src/python/test_qgsnurbscurve.py @@ -0,0 +1,298 @@ +"""QGIS Unit tests for QgsNurbsCurve. + +.. note:: 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. +""" + +__author__ = "Loic Bartoletti" +__date__ = "10/09/2025" +__copyright__ = "Copyright 2025, The QGIS Project" + + +import qgis # NOQA + +from qgis.core import ( + QgsNurbsCurve, + QgsCurve, + QgsPoint, + QgsLineString, + QgsGeometry, + QgsWkbTypes, + QgsVertexId, + Qgis, +) +from qgis.PyQt.QtCore import QByteArray +import binascii + +from qgis.testing import start_app, unittest +from utilities import unitTestDataPath + +start_app() + + +class TestQgsNurbsCurve(unittest.TestCase): + + def testConstructor(self): + """Test QgsNurbsCurve constructor""" + # Test empty constructor + curve = QgsNurbsCurve() + self.assertEqual(curve.numPoints(), 0) + self.assertEqual(curve.degree(), 0) + self.assertEqual(len(curve.controlPoints()), 0) + self.assertEqual(len(curve.knots()), 0) + self.assertEqual(len(curve.weights()), 0) + + # Test constructor with parameters + control_points = [ + QgsPoint(0, 0), + QgsPoint(1, 1), + QgsPoint(2, 0), + QgsPoint(3, 1), + ] + degree = 2 + knots = [0, 0, 0, 1, 2, 2, 2] # degree + 1 + control_points = 2 + 1 + 4 = 7 + weights = [1, 1, 1, 1] + + nurbs = QgsNurbsCurve(control_points, degree, knots, weights) + self.assertEqual(nurbs.degree(), 2) + self.assertEqual(len(nurbs.controlPoints()), 4) + self.assertEqual(len(nurbs.knots()), 7) + self.assertEqual(len(nurbs.weights()), 4) + + def testClone(self): + """Test clone method""" + # Test with a simple linear case first + control_points = [QgsPoint(0, 0), QgsPoint(10, 10)] + degree = 1 + knots = [0, 0, 1, 1] # Simple linear knot vector + weights = [1, 1] + + original = QgsNurbsCurve(control_points, degree, knots, weights) + cloned = original.clone() + + self.assertIsInstance(cloned, QgsCurve) + self.assertEqual(cloned.geometryType(), "NurbsCurve") + + if not original.isEmpty() and not cloned.isEmpty(): + self.assertAlmostEqual(original.length(), cloned.length(), places=5) + + def testGeometryType(self): + """Test geometry type""" + curve = QgsNurbsCurve() + self.assertEqual(curve.geometryType(), "NurbsCurve") + + def testProperties(self): + """Test NURBS curve properties""" + # Test B-spline (all weights = 1) + control_points = [QgsPoint(0, 0), QgsPoint(1, 1), QgsPoint(2, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + bspline = QgsNurbsCurve(control_points, degree, knots, weights) + self.assertTrue(bspline.isBSpline()) + self.assertFalse(bspline.isRational()) + + # Test rational curve (non-uniform weights) + rational_weights = [1, 2, 1] + rational = QgsNurbsCurve(control_points, degree, knots, rational_weights) + self.assertFalse(rational.isBSpline()) + self.assertTrue(rational.isRational()) + + def testBezierCurve(self): + """Test Bézier curve detection""" + # Cubic Bézier curve + control_points = [ + QgsPoint(0, 0), + QgsPoint(1, 2), + QgsPoint(2, 2), + QgsPoint(3, 0), + ] + degree = 3 + knots = [0, 0, 0, 0, 1, 1, 1, 1] # Bézier knot vector + weights = [1, 1, 1, 1] + + bezier = QgsNurbsCurve(control_points, degree, knots, weights) + self.assertTrue(bezier.isBezier()) + + def testEvaluation(self): + """Test curve evaluation at parameter t""" + # Simple linear case (degree 1) + control_points = [QgsPoint(0, 0), QgsPoint(10, 10)] + degree = 1 + knots = [0, 0, 1, 1] + weights = [1, 1] + + linear = QgsNurbsCurve(control_points, degree, knots, weights) + + # Test evaluation at different parameters + start_point = linear.evaluate(0.0) + self.assertEqual(start_point.x(), 0.0) + self.assertEqual(start_point.y(), 0.0) + + mid_point = linear.evaluate(0.5) + self.assertEqual(mid_point.x(), 5.0) + self.assertEqual(mid_point.y(), 5.0) + + end_point = linear.evaluate(1.0) + self.assertEqual(end_point.x(), 10.0) + self.assertEqual(end_point.y(), 10.0) + + def testStartEndPoints(self): + """Test start and end points""" + # Simple linear case + control_points = [QgsPoint(0, 0), QgsPoint(10, 5)] + degree = 1 + knots = [0, 0, 1, 1] + weights = [1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + + start = curve.startPoint() + end = curve.endPoint() + + # For a linear NURBS curve, start/end should be at first/last control points + self.assertAlmostEqual(start.x(), 0.0, places=5) + self.assertAlmostEqual(start.y(), 0.0, places=5) + self.assertAlmostEqual(end.x(), 10.0, places=5) + self.assertAlmostEqual(end.y(), 5.0, places=5) + + def testNumPoints(self): + """Test number of points""" + control_points = [QgsPoint(0, 0), QgsPoint(1, 1), QgsPoint(2, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + # numPoints might return discretized points or control points count + # depending on implementation + self.assertGreater(curve.numPoints(), 0) + + def testToLineString(self): + """Test conversion to line string""" + control_points = [QgsPoint(0, 0), QgsPoint(1, 1), QgsPoint(2, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + line_string = curve.curveToLine() + + self.assertIsInstance(line_string, QgsLineString) + self.assertGreater(line_string.numPoints(), 2) + + # Check that start and end points match + self.assertEqual(line_string.startPoint().x(), curve.startPoint().x()) + self.assertEqual(line_string.startPoint().y(), curve.startPoint().y()) + self.assertEqual(line_string.endPoint().x(), curve.endPoint().x()) + self.assertEqual(line_string.endPoint().y(), curve.endPoint().y()) + + def testCurveToLine(self): + """Test curveToLine method""" + control_points = [QgsPoint(0, 0), QgsPoint(5, 5), QgsPoint(10, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + line = curve.curveToLine() + + self.assertIsInstance(line, QgsLineString) + self.assertGreater(line.numPoints(), 2) + + def testReversed(self): + """Test reversed curve""" + control_points = [QgsPoint(0, 0), QgsPoint(5, 5), QgsPoint(10, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + original = QgsNurbsCurve(control_points, degree, knots, weights) + reversed_curve = original.reversed() + + # Start and end should be swapped + self.assertEqual(original.startPoint().x(), reversed_curve.endPoint().x()) + self.assertEqual(original.startPoint().y(), reversed_curve.endPoint().y()) + self.assertEqual(original.endPoint().x(), reversed_curve.startPoint().x()) + self.assertEqual(original.endPoint().y(), reversed_curve.startPoint().y()) + + def testBoundingBox(self): + """Test bounding box calculation""" + # Quadratic Bézier curve: the curve reaches max y=5 at t=0.5 + # The control points define the convex hull, but the curve doesn't pass through them + control_points = [QgsPoint(0, 0), QgsPoint(5, 10), QgsPoint(10, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + bbox = curve.boundingBox() + + self.assertLessEqual(bbox.xMinimum(), 0.0) + self.assertGreaterEqual(bbox.xMaximum(), 10.0) + self.assertLessEqual(bbox.yMinimum(), 0.0) + # For a quadratic Bézier with these control points, max y is 5.0 (at t=0.5) + self.assertGreaterEqual(bbox.yMaximum(), 4.9) # Allow small tolerance + + def testEquals(self): + """Test equality comparison""" + control_points = [QgsPoint(0, 0), QgsPoint(1, 1), QgsPoint(2, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve1 = QgsNurbsCurve(control_points, degree, knots, weights) + curve2 = QgsNurbsCurve(control_points, degree, knots, weights) + curve3 = QgsNurbsCurve( + [QgsPoint(0, 0), QgsPoint(2, 2)], 1, [0, 0, 1, 1], [1, 1] + ) + + self.assertTrue(curve1.equals(curve2)) + self.assertFalse(curve1.equals(curve3)) + + def testIsEmpty(self): + """Test isEmpty method""" + empty_curve = QgsNurbsCurve() + self.assertTrue(empty_curve.isEmpty()) + + control_points = [QgsPoint(0, 0), QgsPoint(1, 1)] + degree = 1 + knots = [0, 0, 1, 1] + weights = [1, 1] + curve = QgsNurbsCurve(control_points, degree, knots, weights) + self.assertFalse(curve.isEmpty()) + + def testLength(self): + """Test curve length calculation""" + # Simple linear case + control_points = [QgsPoint(0, 0), QgsPoint(3, 4)] # 3-4-5 triangle + degree = 1 + knots = [0, 0, 1, 1] + weights = [1, 1] + + linear = QgsNurbsCurve(control_points, degree, knots, weights) + length = linear.length() + self.assertAlmostEqual(length, 5.0, places=2) + + def testHasCurvedSegments(self): + """Test hasCurvedSegments method""" + # Any NURBS curve with degree > 1 should have curved segments + control_points = [QgsPoint(0, 0), QgsPoint(1, 1), QgsPoint(2, 0)] + degree = 2 + knots = [0, 0, 0, 1, 1, 1] + weights = [1, 1, 1] + + curve = QgsNurbsCurve(control_points, degree, knots, weights) + self.assertTrue(curve.hasCurvedSegments()) + + def testDimension(self): + """Test dimension method""" + curve = QgsNurbsCurve() + self.assertEqual(curve.dimension(), 1) # Curves are 1-dimensional + + +if __name__ == "__main__": + unittest.main()