qgsgeometryutils(chamfer/fillet): add exception management to chamfer/fillet

This commit is contained in:
bdm-oslandia 2025-08-28 10:32:59 +02:00
parent a267b95c32
commit 3593cbbea9
11 changed files with 387 additions and 133 deletions

View File

@ -66,6 +66,10 @@ try:
QgsGeometryUtils.interpolatePointOnSegment = staticmethod(QgsGeometryUtils.interpolatePointOnSegment)
QgsGeometryUtils.createChamfer = staticmethod(QgsGeometryUtils.createChamfer)
QgsGeometryUtils.createFillet = staticmethod(QgsGeometryUtils.createFillet)
QgsGeometryUtils.createChamferGeometry = staticmethod(QgsGeometryUtils.createChamferGeometry)
QgsGeometryUtils.createFilletGeometry = staticmethod(QgsGeometryUtils.createFilletGeometry)
QgsGeometryUtils.chamferVertex = staticmethod(QgsGeometryUtils.chamferVertex)
QgsGeometryUtils.filletVertex = staticmethod(QgsGeometryUtils.filletVertex)
QgsGeometryUtils.__group__ = ['geometry']
except (NameError, AttributeError):
pass

View File

@ -1190,7 +1190,7 @@ interpolation.
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double distance1, double distance2,
QgsPoint &chamferStart /Out/, QgsPoint &chamferEnd /Out/,
double epsilon = 1e-8 ) /HoldGIL/;
double epsilon = 1e-8 ) /HoldGIL/;
%Docstring
Creates a chamfer between two line segments using :py:class:`QgsPoint`.
@ -1207,6 +1207,8 @@ Creates a chamfer between two line segments using :py:class:`QgsPoint`.
- chamferStart: calculated start point of the chamfer
- chamferEnd: calculated end point of the chamfer
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End
@ -1216,7 +1218,7 @@ Creates a chamfer between two line segments using :py:class:`QgsPoint`.
QgsPoint &filletPoint1 /Out/,
QgsPoint &filletMidPoint /Out/,
QgsPoint &filletPoint2 /Out/,
double epsilon = 1e-8 ) /HoldGIL/;
double epsilon = 1e-8 ) /HoldGIL/;
%Docstring
Creates a fillet (rounded corner) between two line segments using
:py:class:`QgsPoint`. Returns the three fillet arc points
@ -1235,13 +1237,16 @@ parameters to define a CircularString.
- filletMidPoint: midpoint of the fillet arc
- filletPoint2: second tangent point of the fillet arc
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End
static std::unique_ptr< QgsLineString >createChamferGeometry(
static std::unique_ptr< QgsLineString > createChamferGeometry(
const QgsPoint &segment1Start, const QgsPoint &segment1End,
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double distance1, double distance2 );
double distance1, double distance2
);
%Docstring
Creates a complete chamfer geometry connecting two segments.
@ -1256,13 +1261,16 @@ Creates a complete chamfer geometry connecting two segments.
:return: :py:class:`QgsLineString` geometry connecting the segments
through the chamfer
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End
static std::unique_ptr< QgsAbstractGeometry >createFilletGeometry(
static std::unique_ptr< QgsAbstractGeometry > createFilletGeometry(
const QgsPoint &segment1Start, const QgsPoint &segment1End,
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double radius, int segments );
double radius, int segments
);
%Docstring
Creates a complete fillet geometry connecting two segments.
@ -1276,12 +1284,15 @@ Creates a complete fillet geometry connecting two segments.
:return: geometry connecting the segments through the fillet
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End
static std::unique_ptr< QgsAbstractGeometry >chamferVertex(
static std::unique_ptr< QgsAbstractGeometry > chamferVertex(
const QgsCurve *curve, int vertexIndex,
double distance1, double distance2 );
double distance1, double distance2
);
%Docstring
Applies chamfer to a vertex in a curve geometry.
@ -1292,12 +1303,15 @@ Applies chamfer to a vertex in a curve geometry.
:return: new geometry with chamfer applied, or None on failure
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End
static std::unique_ptr< QgsAbstractGeometry >filletVertex(
static std::unique_ptr< QgsAbstractGeometry > filletVertex(
const QgsCurve *curve, int vertexIndex,
double radius, int segments );
double radius, int segments
);
%Docstring
Applies fillet to a vertex in a curve geometry.
@ -1309,6 +1323,8 @@ Applies fillet to a vertex in a curve geometry.
:return: new geometry with fillet applied, or None on failure
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End

View File

@ -66,6 +66,10 @@ try:
QgsGeometryUtils.interpolatePointOnSegment = staticmethod(QgsGeometryUtils.interpolatePointOnSegment)
QgsGeometryUtils.createChamfer = staticmethod(QgsGeometryUtils.createChamfer)
QgsGeometryUtils.createFillet = staticmethod(QgsGeometryUtils.createFillet)
QgsGeometryUtils.createChamferGeometry = staticmethod(QgsGeometryUtils.createChamferGeometry)
QgsGeometryUtils.createFilletGeometry = staticmethod(QgsGeometryUtils.createFilletGeometry)
QgsGeometryUtils.chamferVertex = staticmethod(QgsGeometryUtils.chamferVertex)
QgsGeometryUtils.filletVertex = staticmethod(QgsGeometryUtils.filletVertex)
QgsGeometryUtils.__group__ = ['geometry']
except (NameError, AttributeError):
pass

View File

@ -1190,7 +1190,7 @@ interpolation.
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double distance1, double distance2,
QgsPoint &chamferStart /Out/, QgsPoint &chamferEnd /Out/,
double epsilon = 1e-8 ) /HoldGIL/;
double epsilon = 1e-8 ) throw( QgsInvalidArgumentException ) /HoldGIL/;
%Docstring
Creates a chamfer between two line segments using :py:class:`QgsPoint`.
@ -1207,6 +1207,8 @@ Creates a chamfer between two line segments using :py:class:`QgsPoint`.
- chamferStart: calculated start point of the chamfer
- chamferEnd: calculated end point of the chamfer
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End
@ -1216,7 +1218,7 @@ Creates a chamfer between two line segments using :py:class:`QgsPoint`.
QgsPoint &filletPoint1 /Out/,
QgsPoint &filletMidPoint /Out/,
QgsPoint &filletPoint2 /Out/,
double epsilon = 1e-8 ) /HoldGIL/;
double epsilon = 1e-8 ) throw( QgsInvalidArgumentException ) /HoldGIL/;
%Docstring
Creates a fillet (rounded corner) between two line segments using
:py:class:`QgsPoint`. Returns the three fillet arc points
@ -1235,13 +1237,16 @@ parameters to define a CircularString.
- filletMidPoint: midpoint of the fillet arc
- filletPoint2: second tangent point of the fillet arc
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End
static std::unique_ptr< QgsLineString >createChamferGeometry(
static std::unique_ptr< QgsLineString > createChamferGeometry(
const QgsPoint &segment1Start, const QgsPoint &segment1End,
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double distance1, double distance2 );
double distance1, double distance2
) throw( QgsInvalidArgumentException );
%Docstring
Creates a complete chamfer geometry connecting two segments.
@ -1256,13 +1261,16 @@ Creates a complete chamfer geometry connecting two segments.
:return: :py:class:`QgsLineString` geometry connecting the segments
through the chamfer
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End
static std::unique_ptr< QgsAbstractGeometry >createFilletGeometry(
static std::unique_ptr< QgsAbstractGeometry > createFilletGeometry(
const QgsPoint &segment1Start, const QgsPoint &segment1End,
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double radius, int segments );
double radius, int segments
) throw( QgsInvalidArgumentException );
%Docstring
Creates a complete fillet geometry connecting two segments.
@ -1276,12 +1284,15 @@ Creates a complete fillet geometry connecting two segments.
:return: geometry connecting the segments through the fillet
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End
static std::unique_ptr< QgsAbstractGeometry >chamferVertex(
static std::unique_ptr< QgsAbstractGeometry > chamferVertex(
const QgsCurve *curve, int vertexIndex,
double distance1, double distance2 );
double distance1, double distance2
) throw( QgsInvalidArgumentException );
%Docstring
Applies chamfer to a vertex in a curve geometry.
@ -1292,12 +1303,15 @@ Applies chamfer to a vertex in a curve geometry.
:return: new geometry with chamfer applied, or None on failure
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End
static std::unique_ptr< QgsAbstractGeometry >filletVertex(
static std::unique_ptr< QgsAbstractGeometry > filletVertex(
const QgsCurve *curve, int vertexIndex,
double radius, int segments );
double radius, int segments
) throw( QgsInvalidArgumentException );
%Docstring
Applies fillet to a vertex in a curve geometry.
@ -1309,6 +1323,8 @@ Applies fillet to a vertex in a curve geometry.
:return: new geometry with fillet applied, or None on failure
:raises QgsInvalidArgumentException:
.. versionadded:: 4.0
%End

View File

@ -4538,18 +4538,50 @@ QgsGeometry QgsGeometry::chamfer( int vertexIndex, double distance1, double dist
return QgsGeometry();
}
const QgsCurve *curve = qgsgeometry_cast<const QgsCurve *>( d->geometry->simplifiedTypeRef() );
const QgsCurve *curve;
if ( type() == Qgis::GeometryType::Polygon )
{
if ( const QgsCurvePolygon *cpoly = qgsgeometry_cast<const QgsCurvePolygon *>( d->geometry->simplifiedTypeRef() ) )
curve = qgsgeometry_cast<const QgsCurve *>( cpoly->exteriorRing() );
else
curve = nullptr;
}
else if ( type() == Qgis::GeometryType::Line )
curve = qgsgeometry_cast<const QgsCurve *>( d->geometry->simplifiedTypeRef() );
else
curve = nullptr;
if ( !curve )
{
mLastError = QStringLiteral( "Chamfer needs curve geometry." );
return QgsGeometry();
}
std::unique_ptr<QgsAbstractGeometry> result;
try
{
result = QgsGeometryUtils::chamferVertex( curve, vertexIndex, distance1, distance2 );
}
catch ( QgsInvalidArgumentException &e )
{
mLastError = e.what();
return QgsGeometry();
}
std::unique_ptr<QgsAbstractGeometry> result( QgsGeometryUtils::chamferVertex( curve, vertexIndex, distance1, distance2 ) );
if ( !result )
{
mLastError = QStringLiteral( "Chamfer generates a null geometry." );
return QgsGeometry();
}
if ( type() == Qgis::GeometryType::Polygon )
{
QgsLineString *linestring = qgsgeometry_cast<QgsLineString *>( result.release() );
QgsPolygon *poly = new QgsPolygon( linestring );
QgsDebugMsgLevel( QStringLiteral( "returning polygon" ), 1 );
return QgsGeometry( dynamic_cast<QgsAbstractGeometry *>( poly ) );
}
QgsDebugMsgLevel( QStringLiteral( "returning linestring" ), 1 );
return QgsGeometry( std::move( result ) );
}
@ -4560,18 +4592,50 @@ QgsGeometry QgsGeometry::fillet( int vertexIndex, double radius, int segments )
return QgsGeometry();
}
const QgsCurve *curve = qgsgeometry_cast<const QgsCurve *>( d->geometry->simplifiedTypeRef() );
const QgsCurve *curve;
if ( type() == Qgis::GeometryType::Polygon )
{
if ( const QgsCurvePolygon *cpoly = qgsgeometry_cast<const QgsCurvePolygon *>( d->geometry->simplifiedTypeRef() ) )
curve = qgsgeometry_cast<const QgsCurve *>( cpoly->exteriorRing() );
else
curve = nullptr;
}
else if ( type() == Qgis::GeometryType::Line )
curve = qgsgeometry_cast<const QgsCurve *>( d->geometry->simplifiedTypeRef() );
else
curve = nullptr;
if ( !curve )
{
mLastError = QStringLiteral( "Fillet needs curve geometry." );
return QgsGeometry();
}
std::unique_ptr<QgsAbstractGeometry> result;
try
{
result = QgsGeometryUtils::filletVertex( curve, vertexIndex, radius, segments );
}
catch ( QgsInvalidArgumentException &e )
{
mLastError = e.what();
return QgsGeometry();
}
std::unique_ptr<QgsAbstractGeometry> result( QgsGeometryUtils::filletVertex( curve, vertexIndex, radius, segments ) );
if ( !result )
{
mLastError = QStringLiteral( "Fillet generates a null geometry." );
return QgsGeometry();
}
if ( type() == Qgis::GeometryType::Polygon )
{
QgsLineString *linestring = qgsgeometry_cast<QgsLineString *>( result.release() );
QgsPolygon *poly = new QgsPolygon( linestring );
QgsDebugMsgLevel( QStringLiteral( "returning polygon" ), 1 );
return QgsGeometry( dynamic_cast<QgsAbstractGeometry *>( poly ) );
}
QgsDebugMsgLevel( QStringLiteral( "returning linestring" ), 1 );
return QgsGeometry( std::move( result ) );
}
@ -4579,11 +4643,22 @@ QgsGeometry QgsGeometry::chamfer( const QgsPoint &segment1Start, const QgsPoint
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double distance1, double distance2 ) const
{
std::unique_ptr<QgsLineString> result( QgsGeometryUtils::createChamferGeometry(
segment1Start, segment1End, segment2Start, segment2End, distance1, distance2 ) );
std::unique_ptr<QgsLineString> result;
try
{
result = QgsGeometryUtils::createChamferGeometry(
segment1Start, segment1End, segment2Start, segment2End, distance1, distance2
);
}
catch ( QgsInvalidArgumentException &e )
{
mLastError = e.what();
return QgsGeometry();
}
if ( !result )
{
mLastError = QStringLiteral( "Chamfer generates a null geometry." );
return QgsGeometry();
}
@ -4594,11 +4669,22 @@ QgsGeometry QgsGeometry::fillet( const QgsPoint &segment1Start, const QgsPoint &
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double radius, int segments ) const
{
std::unique_ptr<QgsAbstractGeometry> result( QgsGeometryUtils::createFilletGeometry(
segment1Start, segment1End, segment2Start, segment2End, radius, segments ) );
std::unique_ptr<QgsAbstractGeometry> result;
try
{
result = QgsGeometryUtils::createFilletGeometry(
segment1Start, segment1End, segment2Start, segment2End, radius, segments
);
}
catch ( QgsInvalidArgumentException &e )
{
mLastError = e.what();
return QgsGeometry();
}
if ( !result )
{
mLastError = QStringLiteral( "Fillet generates a null geometry." );
return QgsGeometry();
}

View File

@ -28,6 +28,7 @@ email : marco.hugentobler at sourcepole dot com
#include "qgscurve.h"
#include "qgsabstractgeometry.h"
#include "qgsvertexid.h"
#include "qgslogger.h"
#include <memory>
#include <QStringList>
@ -1323,23 +1324,21 @@ bool QgsGeometryUtils::createChamfer( const QgsPoint &segment1Start, const QgsPo
// Validate input parameters
if ( distance1 <= 0 || distance2 <= 0 )
return false;
throw QgsInvalidArgumentException( "Negative distances." );
// Create chamfer points using the utility function
double chamferStartX, chamferStartY, chamferEndX, chamferEndY;
if ( !QgsGeometryUtilsBase::createChamfer(
segment1Start.x(), segment1Start.y(), segment1End.x(), segment1End.y(),
segment2Start.x(), segment2Start.y(), segment2End.x(), segment2End.y(),
distance1, distance2,
chamferStartX, chamferStartY,
chamferEndX, chamferEndY,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
epsilon ) )
{
return false;
}
QgsGeometryUtilsBase::createChamfer(
segment1Start.x(), segment1Start.y(), segment1End.x(), segment1End.y(),
segment2Start.x(), segment2Start.y(), segment2End.x(), segment2End.y(),
distance1, distance2,
chamferStartX, chamferStartY,
chamferEndX, chamferEndY,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
epsilon
);
chamferStart = interpolatePointOnSegment( chamferStartX, chamferStartY, segment1Start, segment1End );
chamferEnd = interpolatePointOnSegment( chamferEndX, chamferEndY, segment2Start, segment2End );
@ -1356,22 +1355,20 @@ bool QgsGeometryUtils::createFillet( const QgsPoint &segment1Start, const QgsPoi
double epsilon )
{
if ( radius <= 0 )
return false;
throw QgsInvalidArgumentException( "Radius <= 0." );
// Create fillet arc using the utility function
double filletPointsX[3], filletPointsY[3];
if ( !QgsGeometryUtilsBase::createFillet(
segment1Start.x(), segment1Start.y(), segment1End.x(), segment1End.y(),
segment2Start.x(), segment2Start.y(), segment2End.x(), segment2End.y(),
radius,
filletPointsX, filletPointsY,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
epsilon ) )
{
return false;
}
QgsGeometryUtilsBase::createFillet(
segment1Start.x(), segment1Start.y(), segment1End.x(), segment1End.y(),
segment2Start.x(), segment2Start.y(), segment2End.x(), segment2End.y(),
radius,
filletPointsX, filletPointsY,
nullptr, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr,
epsilon
);
filletPoint1 = interpolatePointOnSegment( filletPointsX[0], filletPointsY[0], segment1Start, segment1End );
filletMidPoint = createPointWithMatchingDimensions( filletPointsX[1], filletPointsY[1], segment1Start );
@ -1397,14 +1394,11 @@ bool QgsGeometryUtils::createFilletArray( const QgsPoint &segment1Start, const Q
double epsilon )
{
QgsPoint p1, p2, p3;
if ( createFillet( segment1Start, segment1End, segment2Start, segment2End, radius, p1, p2, p3, epsilon ) )
{
filletPoints[0] = p1;
filletPoints[1] = p2;
filletPoints[2] = p3;
return true;
}
return false;
createFillet( segment1Start, segment1End, segment2Start, segment2End, radius, p1, p2, p3, epsilon );
filletPoints[0] = p1;
filletPoints[1] = p2;
filletPoints[2] = p3;
return true;
}
std::unique_ptr<QgsLineString> QgsGeometryUtils::createChamferGeometry(
@ -1413,8 +1407,7 @@ std::unique_ptr<QgsLineString> QgsGeometryUtils::createChamferGeometry(
double distance1, double distance2 )
{
QgsPoint chamferStart, chamferEnd;
if ( !createChamfer( segment1Start, segment1End, segment2Start, segment2End, distance1, distance2, chamferStart, chamferEnd ) )
return nullptr;
createChamfer( segment1Start, segment1End, segment2Start, segment2End, distance1, distance2, chamferStart, chamferEnd );
return std::make_unique<QgsLineString>(
QVector<QgsPoint> { segment1Start, chamferStart, chamferEnd, segment2Start } );
@ -1426,8 +1419,7 @@ std::unique_ptr<QgsAbstractGeometry> QgsGeometryUtils::createFilletGeometry(
double radius, int segments )
{
QgsPoint filletPoints[3];
if ( !createFilletArray( segment1Start, segment1End, segment2Start, segment2End, radius, filletPoints ) )
return nullptr;
createFilletArray( segment1Start, segment1End, segment2Start, segment2End, radius, filletPoints );
// Calculate far endpoints for complete geometry
double intersectionX, intersectionY;
@ -1498,15 +1490,17 @@ std::unique_ptr<QgsAbstractGeometry> QgsGeometryUtils::chamferVertex(
const QgsCurve *curve, int vertexIndex,
double distance1, double distance2 )
{
if ( !curve || vertexIndex <= 0 || vertexIndex >= curve->numPoints() - 1 )
return nullptr;
if ( !curve )
throw QgsInvalidArgumentException( "Curve is null." );
if ( vertexIndex <= 0 || vertexIndex >= curve->numPoints() - 1 )
throw QgsInvalidArgumentException( "Vertex index out of range." );
// Apply symmetric distance if distance2 is negative
if ( distance2 <= 0 )
distance2 = distance1;
if ( distance1 <= 0 || distance2 <= 0 )
return nullptr;
throw QgsInvalidArgumentException( "Negative distances." );
// Extract the three consecutive vertices
const QgsPoint pPrev = curve->vertexAt( QgsVertexId( 0, 0, vertexIndex - 1 ) );
@ -1515,8 +1509,7 @@ std::unique_ptr<QgsAbstractGeometry> QgsGeometryUtils::chamferVertex(
// Create chamfer
QgsPoint chamferStart, chamferEnd;
if ( !createChamfer( pPrev, p, p, pNext, distance1, distance2, chamferStart, chamferEnd ) )
return nullptr;
createChamfer( pPrev, p, p, pNext, distance1, distance2, chamferStart, chamferEnd );
// Handle LineString geometries
if ( qgsgeometry_cast<const QgsLineString *>( curve ) )
@ -1568,7 +1561,7 @@ std::unique_ptr<QgsAbstractGeometry> QgsGeometryUtils::chamferVertex(
if ( targetCurveIndex == -1 )
{
return nullptr;
throw QgsInvalidArgumentException( "While generating output: unable to find curve within compound." );
}
const QgsCurve *targetCurve = compound->curveAt( targetCurveIndex );
@ -1625,25 +1618,31 @@ std::unique_ptr<QgsAbstractGeometry> QgsGeometryUtils::chamferVertex(
return newCompound;
}
return nullptr;
throw QgsInvalidArgumentException( "While generating output: curse is not a QgsLineString nor a QgsCompoundCurve." );
}
std::unique_ptr<QgsAbstractGeometry> QgsGeometryUtils::filletVertex(
const QgsCurve *curve, int vertexIndex,
double radius, int segments )
{
if ( !curve || vertexIndex <= 0 || vertexIndex >= curve->numPoints() - 1 || radius <= 0 )
return nullptr;
if ( !curve )
throw QgsInvalidArgumentException( "Curve is null." );
if ( vertexIndex <= 0 || vertexIndex >= curve->numPoints() - 1 )
throw QgsInvalidArgumentException( "Vertex index out of range." );
if ( radius <= 0 )
throw QgsInvalidArgumentException( "Radius <= 0." );
// Extract the three consecutive vertices
const QgsPoint pPrev = curve->vertexAt( QgsVertexId( 0, 0, vertexIndex - 1 ) );
const QgsPoint p = curve->vertexAt( QgsVertexId( 0, 0, vertexIndex ) );
const QgsPoint pNext = curve->vertexAt( QgsVertexId( 0, 0, vertexIndex + 1 ) );
double rad = std::min( radius, pPrev.distance( p ) * 0.95 );
rad = std::min( rad, pNext.distance( p ) * 0.95 );
// Create fillet
QgsPoint filletPoints[3];
if ( !createFilletArray( pPrev, p, p, pNext, radius, filletPoints ) )
return nullptr;
createFilletArray( pPrev, p, p, pNext, rad, filletPoints );
// Handle LineString geometries
if ( qgsgeometry_cast<const QgsLineString *>( curve ) )
@ -1706,7 +1705,7 @@ std::unique_ptr<QgsAbstractGeometry> QgsGeometryUtils::filletVertex(
if ( targetCurveIndex == -1 )
{
return nullptr;
throw QgsInvalidArgumentException( "While generating output: unable to find curve within compound." );
}
const QgsCurve *targetCurve = compound->curveAt( targetCurveIndex );
@ -1779,5 +1778,5 @@ std::unique_ptr<QgsAbstractGeometry> QgsGeometryUtils::filletVertex(
return newCompound;
}
return nullptr;
throw QgsInvalidArgumentException( "While generating output: curse is not a QgsLineString nor a QgsCompoundCurve." );
}

View File

@ -1308,13 +1308,14 @@ class CORE_EXPORT QgsGeometryUtils
* \param chamferEnd calculated end point of the chamfer
* \param epsilon tolerance for geometric calculations
* \returns true if chamfer was successfully created
* \throws QgsInvalidArgumentException
* \since QGIS 4.0
*/
static bool createChamfer( const QgsPoint &segment1Start, const QgsPoint &segment1End,
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double distance1, double distance2,
QgsPoint &chamferStart SIP_OUT, QgsPoint &chamferEnd SIP_OUT,
double epsilon = 1e-8 ) SIP_HOLDGIL;
double epsilon = 1e-8 ) SIP_THROW( QgsInvalidArgumentException ) SIP_HOLDGIL;
/**
* Creates a fillet (rounded corner) between two line segments using QgsPoint.
@ -1330,6 +1331,7 @@ class CORE_EXPORT QgsGeometryUtils
* \param filletPoint2 second tangent point of the fillet arc
* \param epsilon tolerance for geometric calculations
* \returns true if fillet was successfully created
* \throws QgsInvalidArgumentException
* \since QGIS 4.0
*/
static bool createFillet( const QgsPoint &segment1Start, const QgsPoint &segment1End,
@ -1338,7 +1340,7 @@ class CORE_EXPORT QgsGeometryUtils
QgsPoint &filletPoint1 SIP_OUT,
QgsPoint &filletMidPoint SIP_OUT,
QgsPoint &filletPoint2 SIP_OUT,
double epsilon = 1e-8 ) SIP_HOLDGIL;
double epsilon = 1e-8 ) SIP_THROW( QgsInvalidArgumentException ) SIP_HOLDGIL;
/**
* Creates a complete chamfer geometry connecting two segments.
@ -1349,12 +1351,14 @@ class CORE_EXPORT QgsGeometryUtils
* \param distance1 chamfer distance along first segment
* \param distance2 chamfer distance along second segment (if negative, uses distance1)
* \returns QgsLineString geometry connecting the segments through the chamfer
* \throws QgsInvalidArgumentException
* \since QGIS 4.0
*/
static std::unique_ptr< QgsLineString >createChamferGeometry(
static std::unique_ptr< QgsLineString > createChamferGeometry(
const QgsPoint &segment1Start, const QgsPoint &segment1End,
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double distance1, double distance2 );
double distance1, double distance2
) SIP_THROW( QgsInvalidArgumentException );
/**
* Creates a complete fillet geometry connecting two segments.
@ -1365,12 +1369,14 @@ class CORE_EXPORT QgsGeometryUtils
* \param radius fillet radius
* \param segments number of segments for arc discretization (0 for circular arc)
* \returns geometry connecting the segments through the fillet
* \throws QgsInvalidArgumentException
* \since QGIS 4.0
*/
static std::unique_ptr< QgsAbstractGeometry >createFilletGeometry(
static std::unique_ptr< QgsAbstractGeometry > createFilletGeometry(
const QgsPoint &segment1Start, const QgsPoint &segment1End,
const QgsPoint &segment2Start, const QgsPoint &segment2End,
double radius, int segments );
double radius, int segments
) SIP_THROW( QgsInvalidArgumentException );
/**
* Applies chamfer to a vertex in a curve geometry.
@ -1379,11 +1385,13 @@ class CORE_EXPORT QgsGeometryUtils
* \param distance1 chamfer distance along first segment
* \param distance2 chamfer distance along second segment
* \returns new geometry with chamfer applied, or None on failure
* \throws QgsInvalidArgumentException
* \since QGIS 4.0
*/
static std::unique_ptr< QgsAbstractGeometry >chamferVertex(
static std::unique_ptr< QgsAbstractGeometry > chamferVertex(
const QgsCurve *curve, int vertexIndex,
double distance1, double distance2 );
double distance1, double distance2
) SIP_THROW( QgsInvalidArgumentException );
/**
* Applies fillet to a vertex in a curve geometry.
@ -1392,15 +1400,18 @@ class CORE_EXPORT QgsGeometryUtils
* \param radius fillet radius
* \param segments number of segments for arc discretization (0 for circular arc)
* \returns new geometry with fillet applied, or None on failure
* \throws QgsInvalidArgumentException
* \since QGIS 4.0
*/
static std::unique_ptr< QgsAbstractGeometry >filletVertex(
static std::unique_ptr< QgsAbstractGeometry > filletVertex(
const QgsCurve *curve, int vertexIndex,
double radius, int segments );
double radius, int segments
) SIP_THROW( QgsInvalidArgumentException );
/**
* Convenient method of createFillet using array output.
* \note Not available in Python bindings.
* \throws QgsInvalidArgumentException
* \since QGIS 4.0
*/
static bool createFilletArray( const QgsPoint &segment1Start, const QgsPoint &segment1End,

View File

@ -16,6 +16,7 @@ email : loic dot bartoletti at oslandia dot com
#include "qgsgeometryutils_base.h"
#include "qgsvector3d.h"
#include "qgsvector.h"
#include "qgsexception.h"
double QgsGeometryUtilsBase::sqrDistToLine( double ptX, double ptY, double x1, double y1, double x2, double y2, double &minDistX, double &minDistY, double epsilon )
{
@ -759,7 +760,7 @@ bool QgsGeometryUtilsBase::createFillet(
if ( !isIntersection )
{
return false;
throw QgsInvalidArgumentException( "Segments do not intersect." );
}
// Calculate distances from intersection to all segment endpoints
@ -800,7 +801,7 @@ bool QgsGeometryUtilsBase::createFillet(
// Validate angle - must be meaningful for fillet creation
if ( std::abs( angle ) < epsilon || std::abs( angle - M_PI ) < epsilon )
{
return false; // Parallel or anti-parallel rays
throw QgsInvalidArgumentException( "Parallel or anti-parallel segments." );
}
// Use the interior angle (always ≤ π) for fillet calculations
@ -813,7 +814,8 @@ bool QgsGeometryUtilsBase::createFillet(
const double halfAngle = workingAngle / 2.0;
if ( std::abs( std::sin( halfAngle ) ) < epsilon )
{
return false; // Avoid division by very small numbers
// Avoid division by very small numbers.
throw QgsInvalidArgumentException( "Segment angle near 0 will generate wrong division" );
}
// Calculate distance from intersection to tangent points using trigonometry
@ -833,12 +835,12 @@ bool QgsGeometryUtilsBase::createFillet(
// This allows fillets on non-touching segments (like chamfer behavior)
if ( intersectionOnSeg1 && distanceToTangent > maxDist1 - epsilon )
{
return false;
throw QgsInvalidArgumentException( "Intersection 1 on segment but too far." );
}
if ( intersectionOnSeg2 && distanceToTangent > maxDist2 - epsilon )
{
return false;
throw QgsInvalidArgumentException( "Intersection 2 on segment but too far." );
}
// Calculate tangent points along the rays
@ -929,13 +931,13 @@ bool QgsGeometryUtilsBase::createChamfer(
double *trim2EndX, double *trim2EndY,
const double epsilon )
{
// Apply symmetric distance if distance2 is negative
if ( distance2 <= 0 )
distance2 = distance1;
// Only for positive distance
if ( distance1 < 0 )
{
return false;
}
if ( distance1 <= 0 || distance2 <= 0 )
throw QgsInvalidArgumentException( "Negative distances." );
// Find intersection point between segments (or their infinite line extensions)
double intersectionX, intersectionY;
@ -947,7 +949,7 @@ bool QgsGeometryUtilsBase::createChamfer(
if ( !isIntersection )
{
return false;
throw QgsInvalidArgumentException( "Segments do not intersect." );
}
// Apply symmetric distance if distance2 is negative

View File

@ -174,6 +174,8 @@ class TestQgsGeometry : public QgsTest
void wktParser();
void chamferFillet();
private:
//! Must be called before each render test
void initPainterTest();
@ -3184,5 +3186,44 @@ void TestQgsGeometry::wktParser()
QVERIFY( mline.fromWkt( "MultiLineString EMPTY" ) );
QCOMPARE( mline.asWkt(), QStringLiteral( "MultiLineString EMPTY" ) );
}
void TestQgsGeometry::chamferFillet()
{
QgsGeometry g, g2;
g = QgsGeometry::fromWkt( QStringLiteral( "Point( 4 5 )" ) );
QCOMPARE( g.lastError(), "" );
g.chamfer( 1, 0.5, 0.5 );
QCOMPARE( g.lastError(), "Chamfer needs curve geometry." );
g = QgsGeometry::fromWkt( QStringLiteral( "LineString(0 1, 1 2))" ) );
QCOMPARE( g.lastError(), "" );
g2 = g.chamfer( 1, 0.5, 0.5 );
QCOMPARE( g.lastError(), "Vertex index out of range." );
g = QgsGeometry::fromWkt( QStringLiteral( "LineString(0 1, 1 2))" ) );
QCOMPARE( g.lastError(), "" );
g2 = g.chamfer( 5, 0.5, 0.5 );
QCOMPARE( g.lastError(), "Vertex index out of range." );
g = QgsGeometry::fromWkt( QStringLiteral( "LineString(0 1, 1 2, 3 1))" ) );
QCOMPARE( g.lastError(), "" );
g2 = g.chamfer( 1, 0.5, 0.5 );
QCOMPARE( g.lastError(), "" );
QCOMPARE( g2.asWkt( 2 ), "LineString (0 1, 0.65 1.65, 1.45 1.78, 3 1)" );
g = QgsGeometry::fromWkt( QStringLiteral( "Polygon(( 5 15, 10 15, 10 20, 5 20, 5 15 ))" ) );
QCOMPARE( g.lastError(), "" );
g2 = g.chamfer( 1, 3.0, 2.5 );
QCOMPARE( g.lastError(), "" );
QCOMPARE( g2.asWkt( 2 ), "Polygon ((5 15, 7 15, 10 17.5, 10 20, 5 20, 5 15))" );
g = QgsGeometry::fromWkt( QStringLiteral( "Polygon(( 5 15, 10 15, 10 20, 5 20, 5 15 ), (6 16, 8 16, 8 18, 6 16 ))" ) );
QCOMPARE( g.lastError(), "" );
g2 = g.chamfer( 1, 3.0, 2.5 );
QCOMPARE( g.lastError(), "" );
QCOMPARE( g2.asWkt( 2 ), "Polygon ((5 15, 7 15, 10 17.5, 10 20, 5 20, 5 15), (6 16, 8 16, 8 18, 6 16 ))" );
}
QGSTEST_MAIN( TestQgsGeometry )
#include "testqgsgeometry.moc"

View File

@ -172,14 +172,26 @@ void TestQgsGeometryUtilsBase::testCreateChamferBase()
double trim1StartX, trim1StartY, trim1EndX, trim1EndY;
double trim2StartX, trim2StartY, trim2EndX, trim2EndY;
bool result = QgsGeometryUtilsBase::createChamfer(
segment1StartX, segment1StartY, segment1EndX, segment1EndY,
segment2StartX, segment2StartY, segment2EndX, segment2EndY,
distance1, distance2,
chamferStartX, chamferStartY, chamferEndX, chamferEndY,
&trim1StartX, &trim1StartY, &trim1EndX, &trim1EndY,
&trim2StartX, &trim2StartY, &trim2EndX, &trim2EndY
);
bool result;
try
{
result = QgsGeometryUtilsBase::createChamfer(
segment1StartX, segment1StartY, segment1EndX, segment1EndY,
segment2StartX, segment2StartY, segment2EndX, segment2EndY,
distance1, distance2,
chamferStartX, chamferStartY, chamferEndX, chamferEndY,
&trim1StartX, &trim1StartY, &trim1EndX, &trim1EndY,
&trim2StartX, &trim2StartY, &trim2EndX, &trim2EndY
);
}
catch ( QgsInvalidArgumentException &e )
{
result = false;
}
catch ( ... )
{
QVERIFY2( false, "Caught unhandled exception" );
}
QCOMPARE( result, expectedSuccess );
@ -315,14 +327,26 @@ void TestQgsGeometryUtilsBase::testCreateFilletBase()
double trim1StartX, trim1StartY, trim1EndX, trim1EndY;
double trim2StartX, trim2StartY, trim2EndX, trim2EndY;
bool result = QgsGeometryUtilsBase::createFillet(
segment1StartX, segment1StartY, segment1EndX, segment1EndY,
segment2StartX, segment2StartY, segment2EndX, segment2EndY,
radius,
filletPointsX, filletPointsY,
&trim1StartX, &trim1StartY, &trim1EndX, &trim1EndY,
&trim2StartX, &trim2StartY, &trim2EndX, &trim2EndY
);
bool result;
try
{
result = QgsGeometryUtilsBase::createFillet(
segment1StartX, segment1StartY, segment1EndX, segment1EndY,
segment2StartX, segment2StartY, segment2EndX, segment2EndY,
radius,
filletPointsX, filletPointsY,
&trim1StartX, &trim1StartY, &trim1EndX, &trim1EndY,
&trim2StartX, &trim2StartY, &trim2EndX, &trim2EndY
);
}
catch ( QgsInvalidArgumentException &e )
{
result = false;
}
catch ( ... )
{
QVERIFY2( false, "Caught unhandled exception" );
}
QCOMPARE( result, expectedSuccess );

View File

@ -12,6 +12,8 @@ __copyright__ = "Copyright 2025, The QGIS Project"
from qgis.core import (
Qgis,
QgsInvalidArgumentException,
QgsGeometryUtils,
QgsCircularString,
QgsCompoundCurve,
QgsGeometry,
@ -226,11 +228,12 @@ class TestQgsGeometry(QgisTestCase):
segment2_start = QgsPoint(0.0, 1.0)
segment2_end = QgsPoint(1.0, 1.0)
result = QgsGeometry().chamfer(
g = QgsGeometry()
result = g.chamfer(
segment1_start, segment1_end, segment2_start, segment2_end, 0.1, 0.1
)
self.assertTrue(result.isEmpty())
self.assertEqual(g.lastError(), "Segments do not intersect.")
def test_chamfer_segments_negative_distances(self):
"""Test that negative distances return empty geometry"""
@ -243,11 +246,12 @@ class TestQgsGeometry(QgisTestCase):
segment2_start = QgsPoint(0.0, 1.0)
segment2_end = QgsPoint(0.0, 0.0)
result = QgsGeometry().chamfer(
g = QgsGeometry()
result = g.chamfer(
segment1_start, segment1_end, segment2_start, segment2_end, -0.1, 0.1
)
self.assertTrue(result.isEmpty())
self.assertEqual(g.lastError(), "Negative distances.")
# CHAMFER TESTS - VERTEX-BASED OVERLOAD
@ -283,8 +287,8 @@ class TestQgsGeometry(QgisTestCase):
linestring = QgsGeometry.fromWkt("LineString (0 0, 1 0, 1 1)")
result = linestring.chamfer(0, 0.1, 0.1)
self.assertTrue(result.isEmpty())
self.assertEqual(linestring.lastError(), "Vertex index out of range.")
def test_chamfer_vertex_invalid_index_last(self):
"""Test chamfer with invalid vertex index (last vertex)"""
@ -293,6 +297,7 @@ class TestQgsGeometry(QgisTestCase):
result = linestring.chamfer(2, 0.1, 0.1)
self.assertTrue(result.isEmpty())
self.assertEqual(linestring.lastError(), "Vertex index out of range.")
def test_chamfer_vertex_acute_angle(self):
"""Test chamfer at vertex with acute angle"""
@ -312,6 +317,15 @@ class TestQgsGeometry(QgisTestCase):
expected_wkt = "LineString (0 0, 2 0, 3.7 0, 4 0.3, 4 2, 4 4)"
self.assertEqual(result.asWkt(2), expected_wkt)
def test_geometryutils_chamfer_vertex_complex_linestring(self):
"""Test chamfer on complex linestring with multiple vertices from QgsGeometryUtils."""
linestring = QgsGeometry.fromWkt("LineString (0 0, 2 0, 4 0, 4 2, 4 4)")
result = QgsGeometryUtils.chamferVertex(linestring.get(), 2, 0.3, 0.3)
expected_wkt = "LineString (0 0, 2 0, 3.7 0, 4 0.3, 4 2, 4 4)"
self.assertEqual(result.asWkt(2), expected_wkt)
# FILLET TESTS - SEGMENT-BASED OVERLOAD
def test_fillet_segments_basic_right_angle_non_touching(self):
@ -571,11 +585,12 @@ class TestQgsGeometry(QgisTestCase):
segment2_start = QgsPoint(0.0, 0.1)
segment2_end = QgsPoint(0.0, 0.0)
result = QgsGeometry().fillet(
g = QgsGeometry()
result = g.fillet(
segment1_start, segment1_end, segment2_start, segment2_end, 1.0
)
self.assertTrue(result.isEmpty())
self.assertEqual(g.lastError(), "Intersection 1 on segment but too far.")
def test_fillet_segments_zero_radius_failure(self):
"""Test that zero radius returns empty geometry"""
@ -588,11 +603,12 @@ class TestQgsGeometry(QgisTestCase):
segment2_start = QgsPoint(0.0, 1.0)
segment2_end = QgsPoint(0.0, 0.0)
result = QgsGeometry().fillet(
g = QgsGeometry()
result = g.fillet(
segment1_start, segment1_end, segment2_start, segment2_end, 0.0
)
self.assertTrue(result.isEmpty())
self.assertEqual(g.lastError(), "Radius <= 0.")
def test_fillet_segments_acute_angle(self):
"""Test fillet creation with acute angle"""
@ -662,24 +678,26 @@ class TestQgsGeometry(QgisTestCase):
linestring = QgsGeometry.fromWkt("LineString (0 0, 1 0, 1 1)")
result = linestring.fillet(0, 0.1)
self.assertTrue(result.isEmpty())
self.assertEqual(linestring.lastError(), "Vertex index out of range.")
def test_fillet_vertex_invalid_index_last(self):
"""Test fillet with invalid vertex index (last vertex)"""
linestring = QgsGeometry.fromWkt("LineString (0 0, 1 0, 1 1)")
result = linestring.fillet(2, 0.1)
self.assertTrue(result.isEmpty())
self.assertEqual(linestring.lastError(), "Vertex index out of range.")
def test_fillet_vertex_large_radius_failure(self):
"""Test fillet with radius too large for available geometry"""
linestring = QgsGeometry.fromWkt("LineString (0 0, 0.1 0, 0.1 0.1)")
result = linestring.fillet(1, 1.0)
self.assertFalse(result.isEmpty())
self.assertTrue(result.isEmpty())
expected_wkt = "LineString (0 0, 0.02 0, 0.04 0.01, 0.06 0.02, 0.07 0.03, 0.08 0.04, 0.09 0.06, 0.1 0.08, 0.1 0.1)"
self.assertEqual(result.asWkt(2), expected_wkt)
# EDGE CASES AND ERROR HANDLING
@ -712,7 +730,6 @@ class TestQgsGeometry(QgisTestCase):
point = QgsGeometry.fromWkt("Point (0 0)")
result = point.fillet(0, 0.1)
self.assertTrue(result.isEmpty())
def test_two_point_linestring_chamfer(self):
@ -720,32 +737,66 @@ class TestQgsGeometry(QgisTestCase):
linestring = QgsGeometry.fromWkt("LineString (0 0, 1 1)")
result = linestring.chamfer(1, 0.1, 0.1)
self.assertTrue(result.isEmpty())
self.assertEqual(linestring.lastError(), "Vertex index out of range.")
def test_two_point_linestring_fillet(self):
"""Test fillet behavior with minimum linestring"""
linestring = QgsGeometry.fromWkt("LineString (0 0, 1 1)")
result = linestring.fillet(1, 0.1)
self.assertTrue(result.isEmpty())
self.assertEqual(linestring.lastError(), "Vertex index out of range.")
def test_geometryutils_two_point_linestring_fillet(self):
"""Test fillet behavior with minimum linestring from QgsGeometryUtils. Should throw exception"""
linestring = QgsGeometry.fromWkt("LineString (0 0, 1 1)")
try:
result = QgsGeometryUtils.filletVertex(linestring.get(), 1, 0.1, 8)
self.fail("Should have failed!")
except QgsInvalidArgumentException as e:
self.assertEqual(e.__str__(), "Vertex index out of range.")
def test_geometryutils_two_point_linestring_chamfer(self):
"""Test chamfer behavior with minimum linestring from QgsGeometryUtils. Should throw exception"""
linestring = QgsGeometry.fromWkt("LineString (0 0, 1 1)")
try:
result = QgsGeometryUtils.chamferVertex(linestring.get(), 1, 0.1, 0.1)
self.fail("Should have failed!")
except QgsInvalidArgumentException as e:
self.assertEqual(e.__str__(), "Vertex index out of range.")
def test_geometryutils_bad_distance_linestring_chamfer(self):
"""Test chamfer behavior with minimum linestring from QgsGeometryUtils. Should throw exception"""
linestring = QgsGeometry.fromWkt("LineString (0 0, 1 1, 1 2, 2 3)")
try:
result = QgsGeometryUtils.chamferVertex(linestring.get(), 1, -0.1, -0.1)
self.fail("Should have failed!")
except QgsInvalidArgumentException as e:
self.assertEqual(e.__str__(), "Negative distances.")
def test_polygon_geometry_handling_chamfer(self):
"""Test chamfer behavior with polygon geometries (should fail gracefully)"""
"""Test chamfer behavior with polygon geometries"""
polygon = QgsGeometry.fromWkt("Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))")
result = polygon.chamfer(1, 0.1)
self.assertFalse(result.isEmpty())
self.assertTrue(result.isEmpty())
expected_wkt = "Polygon ((0 0, 0.9 0, 1 0.1, 1 1, 0 1, 0 0))"
self.assertEqual(result.asWkt(2), expected_wkt)
def test_polygon_geometry_handling_fillet(self):
"""Test fillet behavior with polygon geometries (should fail gracefully)"""
polygon = QgsGeometry.fromWkt("Polygon ((0 0, 1 0, 1 1, 0 1, 0 0))")
result = polygon.fillet(1, 0.1)
self.assertFalse(result.isEmpty())
self.assertTrue(result.isEmpty())
expected_wkt = "Polygon ((0 0, 0.92 0, 0.93 0.01, 0.95 0.01, 0.96 0.02, 0.98 0.04, 0.99 0.05, 0.99 0.07, 1 0.08, 1 1, 0 1, 0 0))"
self.assertEqual(result.asWkt(2), expected_wkt)
# PRECISION AND PERFORMANCE TESTS