From b85fab9ab6f49a9235402a811dc2c46d1bea36f9 Mon Sep 17 00:00:00 2001 From: Jon Evans Date: Tue, 20 Dec 2022 17:21:20 -0500 Subject: [PATCH] Support DXF ellipses and elliptical arcs Fixes https://gitlab.com/kicad/code/kicad/-/issues/12563 --- libs/kimath/CMakeLists.txt | 1 + libs/kimath/include/bezier_curves.h | 34 +++++ libs/kimath/include/geometry/ellipse.h | 76 +++++++++++ libs/kimath/src/bezier_curves.cpp | 98 ++++++++++++++ libs/kimath/src/geometry/ellipse.cpp | 51 +++++++ pcbnew/import_gfx/dxf_import_plugin.cpp | 68 ++++++++++ pcbnew/import_gfx/dxf_import_plugin.h | 3 +- qa/unittests/libs/kimath/CMakeLists.txt | 1 + .../geometry/test_ellipse_to_bezier.cpp | 125 ++++++++++++++++++ 9 files changed, 456 insertions(+), 1 deletion(-) create mode 100644 libs/kimath/include/geometry/ellipse.h create mode 100644 libs/kimath/src/geometry/ellipse.cpp create mode 100644 qa/unittests/libs/kimath/geometry/test_ellipse_to_bezier.cpp diff --git a/libs/kimath/CMakeLists.txt b/libs/kimath/CMakeLists.txt index 5e99e1725a..c5605fb040 100644 --- a/libs/kimath/CMakeLists.txt +++ b/libs/kimath/CMakeLists.txt @@ -5,6 +5,7 @@ set( KIMATH_SRCS src/trigo.cpp src/geometry/eda_angle.cpp + src/geometry/ellipse.cpp src/geometry/circle.cpp src/geometry/convex_hull.cpp src/geometry/direction_45.cpp diff --git a/libs/kimath/include/bezier_curves.h b/libs/kimath/include/bezier_curves.h index 2370553bf2..6101d13423 100644 --- a/libs/kimath/include/bezier_curves.h +++ b/libs/kimath/include/bezier_curves.h @@ -27,6 +27,8 @@ #include #include +template class ELLIPSE; + /** * Bezier curves to polygon converter. * @@ -65,4 +67,36 @@ private: std::vector m_ctrlPts; }; + +// TODO: Refactor BEZIER_POLY to use BEZIER + +/** + * Generic cubic Bezier representation + */ +template +class BEZIER +{ +public: + BEZIER() = default; + + BEZIER( VECTOR2 aStart, VECTOR2 aC1, VECTOR2 aC2, + VECTOR2 aEnd ) : + Start( aStart ), + C1( aC1 ), + C2( aC2 ), + End( aEnd ) + {} + + VECTOR2 Start; + VECTOR2 C1; + VECTOR2 C2; + VECTOR2 End; +}; + +/** + * Transforms an ellipse or elliptical arc into a set of quadratic Bezier curves that approximate it + */ +template +void TransformEllipseToBeziers( const ELLIPSE& aEllipse, std::vector>& aBeziers ); + #endif // BEZIER_CURVES_H diff --git a/libs/kimath/include/geometry/ellipse.h b/libs/kimath/include/geometry/ellipse.h new file mode 100644 index 0000000000..5f73298410 --- /dev/null +++ b/libs/kimath/include/geometry/ellipse.h @@ -0,0 +1,76 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2022 KiCad Developers, see AUTHORS.txt for contributors. + * + * 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 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#ifndef KICAD_ELLIPSE_H +#define KICAD_ELLIPSE_H + +#include +#include + +/** + * This class was created to handle importing ellipses from other file formats that support them + * natively. The storage format and API may need to be refactored before this is used as part of a + * potential future native KiCad ellipse. + */ + +template +class ELLIPSE +{ +public: + ELLIPSE() = default; + + /** + * Constructs an ellipse or elliptical arc. The ellipse sweeps from aStartAngle to aEndAngle + * in a counter-clockwise direction. + * + * @param aCenter is the center point of the ellipse. + * @param aMajorRadius is the radius of the "x-axis" dimension of the ellipse. + * @param aMinorRadius is the radius of the "y-axis" dimension of the ellipse. + * @param aRotation is the angle of the ellipse "x-axis" relative to world x-axis. + * @param aStartAngle is the starting angle of the elliptical arc. + * @param aEndAngle is the ending angle of the elliptical arc. + */ + ELLIPSE( const VECTOR2& aCenter, NumericType aMajorRadius, + NumericType aMinorRadius, EDA_ANGLE aRotation, EDA_ANGLE aStartAngle = ANGLE_0, + EDA_ANGLE aEndAngle = FULL_CIRCLE ); + + /** + * Constructs a DXF-style ellipse or elliptical arc, where the major axis is given by a point + * rather than a radius, and therefore defines not only the major radius but also the rotation + * of the ellipse. + * + * @param aCenter is the center point of the ellipse. + * @param aMajor is the endpoint of the major axis, relative to the center. + * @param aRatio is the ratio of the minor axis length to the major axis length. + * @param aStartAngle is the starting angle of the elliptical arc. + * @param aEndAngle is the ending angle of the elliptical arc. + */ + ELLIPSE( const VECTOR2& aCenter, const VECTOR2& aMajor, double aRatio, + EDA_ANGLE aStartAngle = ANGLE_0, EDA_ANGLE aEndAngle = FULL_CIRCLE ); + + VECTOR2 Center; + NumericType MajorRadius; + NumericType MinorRadius; + EDA_ANGLE Rotation; + EDA_ANGLE StartAngle; + EDA_ANGLE EndAngle; + +}; + +#endif //KICAD_ELLIPSE_H diff --git a/libs/kimath/src/bezier_curves.cpp b/libs/kimath/src/bezier_curves.cpp index 01a907ea26..d64725f7be 100644 --- a/libs/kimath/src/bezier_curves.cpp +++ b/libs/kimath/src/bezier_curves.cpp @@ -26,6 +26,8 @@ /************************************/ #include +#include +#include #include // for VECTOR2D, operator*, VECTOR2 #include // for wxASSERT @@ -105,3 +107,99 @@ void BEZIER_POLY::GetPoly( std::vector& aOutput, double aMinSegLen, in if( aOutput.back() != m_ctrlPts[3] ) aOutput.push_back( m_ctrlPts[3] ); } + + +template +void TransformEllipseToBeziers( const ELLIPSE& aEllipse, std::vector>& aBeziers ) +{ + EDA_ANGLE arcAngle = -( aEllipse.EndAngle - aEllipse.StartAngle ); + + if( arcAngle >= ANGLE_0 ) + arcAngle -= ANGLE_360; + + /* + * KiCad does not natively support ellipses or elliptical arcs. So, we convert them to Bezier + * splines as these are the nearest thing we have that represents them in a way that is both + * editable and preserves their curvature accurately (enough). + * + * Credit to Kliment for developing and documenting this method. + */ + /// Minimum number of Beziers to use for a full circle to keep error manageable. + const int minBeziersPerCircle = 4; + + /// The number of Beziers needed for the given arc + const int numBeziers = std::ceil( std::abs( arcAngle.AsRadians() / + ( 2 * M_PI / minBeziersPerCircle ) ) ); + + /// Angle occupied by each Bezier + const double angleIncrement = arcAngle.AsRadians() / numBeziers; + + /* + * Now, let's assume a circle of radius 1, centered on origin, with angle startangle + * x-axis-aligned. We'll move, scale, and rotate it later. We're creating Bezier curves that hug + * this circle as closely as possible, with the angles that will be used on the final ellipse + * too. + * + * Thanks to the beautiful and excellent https://pomax.github.io/bezierinfo we know how to + * define a curve that hugs a circle as closely as possible. + * + * We need the value k, which is the optimal distance from the endpoint to the control point to + * make the curve match the circle for a given circle arc angle. + * + * k = 4/3 * tan(θ/4), where θ is the angle of the arc. In our case, θ=angleIncrement + */ + double theta = angleIncrement; + double k = ( 4. / 3. ) * std::tan( theta / 4 ); + + /* + * Define our Bezier: + * - Start point is on the circle at the x-axis + * - First control point just uses k as the y-value + * - Second control point is offset from the end point + * - End point is defined by the angle of the arc segment + * Note that we use double here no matter what the template param is; round at the end only. + */ + BEZIER first = { { 1, 0 }, + { 1, k }, + { std::cos( theta ) + k * std::sin( theta ), + std::sin( theta ) - k * std::cos( theta ) }, + { std::cos( theta ), std::sin( theta ) } }; + + /* + * Now construct the actual segments by transforming/rotating the first one + */ + auto transformPoint = + [&]( VECTOR2D aPoint, const double aAngle ) -> VECTOR2D + { + // Bring to the actual starting angle + RotatePoint( aPoint, + -EDA_ANGLE( aAngle - aEllipse.StartAngle.AsRadians(), RADIANS_T ) ); + + // Then scale to the major and minor radiuses of the ellipse + aPoint *= VECTOR2D( aEllipse.MajorRadius, aEllipse.MinorRadius ); + + // Now rotate to the ellipse coordinate system + RotatePoint( aPoint, -aEllipse.Rotation ); + + // And finally offset to the center location of the ellipse + aPoint += aEllipse.Center; + + return aPoint; + }; + + for( int i = 0; i < numBeziers; i++ ) + { + aBeziers.emplace_back( BEZIER( { + transformPoint( first.Start, i * angleIncrement ), + transformPoint( first.C1, i * angleIncrement ), + transformPoint( first.C2, i * angleIncrement ), + transformPoint( first.End, i * angleIncrement ) + } ) ); + } +} + + +template void TransformEllipseToBeziers( const ELLIPSE& aEllipse, + std::vector>& aBeziers ); +template void TransformEllipseToBeziers( const ELLIPSE& aEllipse, + std::vector>& aBeziers ); \ No newline at end of file diff --git a/libs/kimath/src/geometry/ellipse.cpp b/libs/kimath/src/geometry/ellipse.cpp new file mode 100644 index 0000000000..4b9e070ee2 --- /dev/null +++ b/libs/kimath/src/geometry/ellipse.cpp @@ -0,0 +1,51 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2022 KiCad Developers, see AUTHORS.txt for contributors. + * + * 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 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include + + +template +ELLIPSE::ELLIPSE( const VECTOR2& aCenter, NumericType aMajorRadius, + NumericType aMinorRadius, EDA_ANGLE aRotation, EDA_ANGLE aStartAngle, + EDA_ANGLE aEndAngle ) : + Center( aCenter ), + MajorRadius( aMajorRadius ), + MinorRadius( aMinorRadius ), + Rotation( aRotation ), + StartAngle( aStartAngle ), + EndAngle( aEndAngle ) +{ +} + + +template +ELLIPSE::ELLIPSE( const VECTOR2& aCenter, + const VECTOR2& aMajor, double aRatio, + EDA_ANGLE aStartAngle, EDA_ANGLE aEndAngle ) : + Center( aCenter ), + StartAngle( aStartAngle ), + EndAngle( aEndAngle ) +{ + MajorRadius = aMajor.EuclideanNorm(); + MinorRadius = MajorRadius * aRatio; + Rotation = EDA_ANGLE( std::atan2( aMajor.y, aMajor.x ), RADIANS_T ); +} + +template class ELLIPSE; +template class ELLIPSE; diff --git a/pcbnew/import_gfx/dxf_import_plugin.cpp b/pcbnew/import_gfx/dxf_import_plugin.cpp index 14fbe0af8e..359609cc45 100644 --- a/pcbnew/import_gfx/dxf_import_plugin.cpp +++ b/pcbnew/import_gfx/dxf_import_plugin.cpp @@ -32,6 +32,8 @@ #include "dxf_import_plugin.h" #include #include +#include +#include #include #include @@ -561,6 +563,72 @@ void DXF_IMPORT_PLUGIN::addArc( const DL_ArcData& aData ) } +void DXF_IMPORT_PLUGIN::addEllipse( const DL_EllipseData& aData ) +{ + DXF_ARBITRARY_AXIS arbAxis = getArbitraryAxis( getExtrusion() ); + VECTOR3D centerCoords = ocsToWcs( arbAxis, VECTOR3D( aData.cx, aData.cy, aData.cz ) ); + VECTOR3D majorCoords = ocsToWcs( arbAxis, VECTOR3D( aData.mx, aData.my, aData.mz ) ); + + // DXF ellipses store the minor axis length as a ratio to the major axis. + // The major coords are relative to the center point. + // For now, we assume ellipses in the XY plane. + + VECTOR2D center( mapX( centerCoords.x ), mapY( centerCoords.y ) ); + VECTOR2D major( mapX( majorCoords.x ), mapY( majorCoords.y ) ); + + // DXF elliptical arcs store their angles in radians (unlike circular arcs which use degrees) + // The arcs wind CCW as in KiCad. The end angle must be greater than the start angle, and if + // the extrusion direction is negative, the arc winding is CW instead! Note that this is a + // simplification that assumes the DXF is representing a 2D drawing, and would need to be + // revisited if we want to import true 3D drawings and "flatten" them to the 2D KiCad plane + // internally. + EDA_ANGLE startAngle( aData.angle1, RADIANS_T ); + EDA_ANGLE endAngle( aData.angle2, RADIANS_T ); + + if( startAngle > endAngle ) + endAngle += ANGLE_360; + + // TODO: testcases for negative extrusion vector; handle it here + + if( aData.ratio == 1.0 ) + { + double radius = major.EuclideanNorm(); + + if( startAngle == endAngle ) + { + DL_CircleData circle( aData.cx, aData.cy, aData.cz, radius ); + addCircle( circle ); + return; + } + else + { + DL_ArcData arc( aData.cx, aData.cy, aData.cz, radius, + startAngle.AsDegrees(), endAngle.AsDegrees() ); + addArc( arc ); + return; + } + } + + std::vector> splines; + ELLIPSE ellipse( center, major, aData.ratio, startAngle, endAngle ); + + TransformEllipseToBeziers( ellipse, splines ); + + DXF_IMPORT_LAYER* layer = getImportLayer( attributes.getLayer() ); + double lineWidth = lineWeightToWidth( attributes.getWidth(), layer ); + + GRAPHICS_IMPORTER_BUFFER* bufferToUse = m_currentBlock ? &m_currentBlock->m_buffer + : &m_internalImporter; + + for( const BEZIER& b : splines ) + bufferToUse->AddSpline( b.Start, b.C1, b.C2, b.End, lineWidth ); + + // Naive bounding + updateImageLimits( center + major ); + updateImageLimits( center - major ); +} + + void DXF_IMPORT_PLUGIN::addText( const DL_TextData& aData ) { DXF_ARBITRARY_AXIS arbAxis = getArbitraryAxis( getExtrusion() ); diff --git a/pcbnew/import_gfx/dxf_import_plugin.h b/pcbnew/import_gfx/dxf_import_plugin.h index b120976da2..c78749c89d 100644 --- a/pcbnew/import_gfx/dxf_import_plugin.h +++ b/pcbnew/import_gfx/dxf_import_plugin.h @@ -418,11 +418,12 @@ private: virtual void addCircle( const DL_CircleData& aData ) override; virtual void addArc( const DL_ArcData& aData ) override; + void addEllipse( const DL_EllipseData& aData ) override; //virtual void addLWPolyline( const DRW_LWPolyline& aData ) override; virtual void addText( const DL_TextData& aData ) override; virtual void addPolyline( const DL_PolylineData& aData ) override; - /* Inserts blocks where specified by insert data */ + /* Inserts blocks where specified by insert data */ virtual void addInsert( const DL_InsertData& aData ) override; /** diff --git a/qa/unittests/libs/kimath/CMakeLists.txt b/qa/unittests/libs/kimath/CMakeLists.txt index 43e030b6cf..7c61a5ecdf 100644 --- a/qa/unittests/libs/kimath/CMakeLists.txt +++ b/qa/unittests/libs/kimath/CMakeLists.txt @@ -27,6 +27,7 @@ set( QA_KIMATH_SRCS test_kimath.cpp + geometry/test_ellipse_to_bezier.cpp geometry/test_fillet.cpp geometry/test_circle.cpp geometry/test_segment.cpp diff --git a/qa/unittests/libs/kimath/geometry/test_ellipse_to_bezier.cpp b/qa/unittests/libs/kimath/geometry/test_ellipse_to_bezier.cpp new file mode 100644 index 0000000000..1e7ddb9842 --- /dev/null +++ b/qa/unittests/libs/kimath/geometry/test_ellipse_to_bezier.cpp @@ -0,0 +1,125 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright (C) 2022 KiCad Developers, see AUTHORS.txt for contributors. + * + * 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 3 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include +#include +#include + +#include + +BOOST_AUTO_TEST_SUITE( EllipseToBezier ) + + +/// Allows for rounding in the testcases +const double MAX_ERROR = 0.01; + + +struct ELLIPSE_TO_BEZIER_CASE +{ + std::string name; + ELLIPSE input; + std::vector> expected; +}; + + +// clang-format off +static const std::vector cases = { + { + "full circle", + { { 0, 0 }, { 100, 0 }, 1.0, ANGLE_0, FULL_CIRCLE }, + { { { 100, 0 }, { 100, -55.22847498307934 }, { 55.228474983079344, -100 }, { 0, -100 } }, + { { 0, -100 }, { -55.22847498307934, -100 }, { -100, -55.228474983079344 }, { -100, 0 } }, + { { -100, 0 }, { -100, 55.22847498307934 }, { -55.228474983079344, 100 }, { 0, 100 } }, + { { 0, 100 }, { 55.22847498307934, 100 }, { 100, 55.228474983079344 }, { 100, 0 } } + } + }, + { + "ellipse", + { { 0, 0 }, { -100, 0 }, 0.5, ANGLE_0, FULL_CIRCLE }, + { { { -100, 0 }, { -100, 27.61423749153967 }, { -55.228474983079344, 50 }, { 0, 50 } }, + { { 0, 50 }, { 55.22847498307934, 50 }, { 100, 27.614237491539672 }, { 100, 0 } }, + { { 100, 0 }, { 100, -27.61423749153967 }, { 55.228474983079344, -50 }, { 0, -50 } }, + { { 0, -50 }, { -55.22847498307934, -50 }, { -100, -27.614237491539672 }, { -100, 0 } } + } + }, + { + "arc1", + { { 0, 0 }, { 100, 0 }, 0.5, ANGLE_180, FULL_CIRCLE }, + { { { 100, 0 }, { 100, -27.61423749153967 }, { 55.228474983079344, -50 }, { 0, -50 } }, + { { 0, -50 }, { -55.22847498307934, -50 }, { -100, -27.614237491539672 }, { -100, 0 } }, + { { -100, 0 }, { -100, 27.61423749153967 }, { -55.228474983079344, 50 }, { 0, 50 } }, + { { 0, 50 }, { 55.22847498307934, 50 }, { 100, 27.614237491539672 }, { 100, 0 } } + } + }, + { + "arc2", + { { 223, 165 }, { 372, 634 }, 0.96, EDA_ANGLE( 4.437, RADIANS_T ), EDA_ANGLE( 0.401, RADIANS_T ) }, + { { { -463.86, 336.27 }, { -389.81, 608.33 }, { -170.75, 818.49 }, { 99.52, 876.73 } }, + { { 99.52, 876.73 }, { 369.80, 934.98 }, { 643.36, 831.0 }, { 803.07, 609.31 } } + } + }, + { + "arc3", + { { 112.75, 490.24 }, { 304.54, 129.16 }, 7.14, EDA_ANGLE( 1.90, RADIANS_T ), EDA_ANGLE( 1.09, RADIANS_T ) }, + { { { 886.98, -1609.17 }, { 608.61, -1333.45 }, { 110.16, -394.92 }, { -305.88, 636.89 } }, + { { -305.88, 636.89 }, { -721.93, 1668.69 }, { -940.88, 2509.32 }, { -829.87, 2648.63 } }, + { { -829.87, 2648.63 }, { -718.85, 2787.95 }, { -308.47, 2187.55 }, { 152.23, 1211.78 } }, + { { 152.23, 1211.78 }, { 612.93, 236.01 }, { 996.95, -846.11 }, { 1071.24, -1377.92 } } + } + }, +}; +// clang-format on + + +BOOST_AUTO_TEST_CASE( EllipseToBezier ) +{ + for( const ELLIPSE_TO_BEZIER_CASE& c : cases ) + { + BOOST_TEST_CONTEXT( c.name ) + { + std::vector> out; + TransformEllipseToBeziers( c.input, out ); + + BOOST_CHECK_EQUAL( c.expected.size(), out.size() ); + +#if 0 + for( BEZIER& b : out ) + { + BOOST_TEST_MESSAGE( fmt::format( "{{ {{ {}, {} }}, {{ {}, {} }}, {{ {}, {} }}, {{ {}, {} }} }}", + b.Start.x, b.Start.y, b.C1.x, b.C1.y, + b.C2.x, b.C2.y, b.End.x, b.End.y ) ); + } +#endif + + for( size_t i = 0; i < out.size(); i++ ) + { + BOOST_CHECK_LE( ( c.expected[i].Start - out[i].Start ).EuclideanNorm(), + MAX_ERROR ); + BOOST_CHECK_LE( ( c.expected[i].C1 - out[i].C1 ).EuclideanNorm(), + MAX_ERROR ); + BOOST_CHECK_LE( ( c.expected[i].C2 - out[i].C2 ).EuclideanNorm(), + MAX_ERROR ); + BOOST_CHECK_LE( ( c.expected[i].End - out[i].End ).EuclideanNorm(), + MAX_ERROR ); + } + } + } +} + +BOOST_AUTO_TEST_SUITE_END()