SHAPE_LINE_CHAIN splitArc on Insert, Remove and Replace

This ensures that the arc shapes remain correct after removing
a point belonging to an arc or inserting a point in the middle
of an arc.

Simplify implementation of Replace( ..., aP ). Now a Remove
operation followed by an Insert operation.

Improve QA test for SHAPE_LINE_CHAIN Append, Insert and Replace

Implement SHAPE_LINE_CHAIN::splitArc to break up an arc into two

Implement SHAPE_ARC::ConstructFromStartEndCenter and add qa test
This commit is contained in:
Roberto Fernandez Bautista 2021-05-09 23:27:02 +01:00 committed by Jon Evans
parent 2f069c0b19
commit a9a8aa8243
6 changed files with 287 additions and 61 deletions

View File

@ -95,6 +95,19 @@ public:
SHAPE_ARC& ConstructFromStartEndAngle( const VECTOR2I& aStart, const VECTOR2I& aEnd,
double aAngle, double aWidth = 0 );
/**
* Constructs this arc from the given start, end and center.
* @param aStart is the arc starting point
* @param aEnd is the arc endpoint
* @param aCenter is the arc center
* @param aClockwise determines which of the two solutions to construct
* @param aWidth is the arc line thickness
* @return *this
*/
SHAPE_ARC& ConstructFromStartEndCenter( const VECTOR2I& aStart, const VECTOR2I& aEnd,
const VECTOR2I& aCenter, bool aClockwise = false,
double aWidth = 0 );
const VECTOR2I& GetP0() const { return m_start; }
const VECTOR2I& GetP1() const { return m_end; }
const VECTOR2I& GetArcMid() const { return m_mid; }
@ -107,6 +120,8 @@ public:
bool Collide( const VECTOR2I& aP, int aClearance = 0, int* aActual = nullptr,
VECTOR2I* aLocation = nullptr ) const override;
bool IsClockwise() const;
void SetWidth( int aWidth )
{
m_width = aWidth;

View File

@ -790,6 +790,23 @@ protected:
*/
void convertArc( ssize_t aArcIndex );
/**
* Splits an arc into two arcs at aPtIndex. Parameter \p aCoincident controls whether the two
* arcs are to be coincident at aPtIndex or whether a short straight segment should be created
* instead
*
* @param aPtIndex index of the point in the chain in which to split the arc
* @param aCoincident If true, the end point of the first arc will be coincident with the start
point of the second arc at aPtIndex.
If false, the end point of the first arc will be at aPtIndex-1 and the
start point of the second arc will be at aPtIndex, resulting in a short
straight line segment between aPtIndex-1 and aPtIndex.
*/
void splitArc( ssize_t aPtIndex, bool aCoincident = false );
void ammendArc( size_t aArcIndex, VECTOR2I aNewStart, VECTOR2I aNewEnd );
/**
* Create a new Clipper path from the SHAPE_LINE_CHAIN in a given orientation
*/

View File

@ -196,6 +196,34 @@ SHAPE_ARC& SHAPE_ARC::ConstructFromStartEndAngle( const VECTOR2I& aStart, const
}
SHAPE_ARC& SHAPE_ARC::ConstructFromStartEndCenter( const VECTOR2I& aStart, const VECTOR2I& aEnd,
const VECTOR2I& aCenter, bool aClockwise,
double aWidth )
{
VECTOR2I startLine = aStart - aCenter;
VECTOR2I endLine = aEnd - aCenter;
double startangle = NormalizeAnglePos(RAD2DECIDEG( startLine.Angle() ));
double endangle = NormalizeAnglePos(RAD2DECIDEG( endLine.Angle() ));
double angle = endangle - startangle;
if( aClockwise )
angle = NormalizeAngleNeg( angle );
else
angle = NormalizeAnglePos( angle );
m_start = aStart;
m_end = aEnd;
m_mid = aStart;
RotatePoint( m_mid, aCenter, -angle / 2.0 );
update_bbox();
return *this;
}
bool SHAPE_ARC::Collide( const SEG& aSeg, int aClearance, int* aActual, VECTOR2I* aLocation ) const
{
if( aSeg.A == aSeg.B )
@ -321,6 +349,12 @@ const BOX2I SHAPE_ARC::BBox( int aClearance ) const
}
bool SHAPE_ARC::IsClockwise() const
{
return GetCentralAngle() < 0;
}
bool SHAPE_ARC::Collide( const VECTOR2I& aP, int aClearance, int* aActual,
VECTOR2I* aLocation ) const
{

View File

@ -115,6 +115,97 @@ void SHAPE_LINE_CHAIN::convertArc( ssize_t aArcIndex )
}
void SHAPE_LINE_CHAIN::ammendArc( size_t aArcIndex, VECTOR2I aNewStart, VECTOR2I aNewEnd )
{
wxCHECK_MSG( aArcIndex < m_arcs.size(), /* void */,
"Invalid arc index requested." );
SHAPE_ARC& theArc = m_arcs[aArcIndex];
// Try to preseve the centre of the original arc
SHAPE_ARC newArc;
newArc.ConstructFromStartEndCenter( aNewStart, aNewEnd, theArc.GetCenter(),
theArc.IsClockwise() );
m_arcs[aArcIndex] = newArc;
}
void SHAPE_LINE_CHAIN::splitArc( ssize_t aPtIndex, bool aCoincident )
{
if( aPtIndex < 0 )
aPtIndex += m_shapes.size();
if( !IsSharedPt( aPtIndex ) && IsArcStart( aPtIndex ) )
return; // Nothing to do
wxCHECK_MSG( aPtIndex < static_cast<ssize_t>( m_shapes.size() ), /* void */,
"Invalid point index requested." );
if( IsSharedPt( aPtIndex ) || IsArcEnd( aPtIndex ) )
{
if( aCoincident || aPtIndex == 0 )
return; // nothing to do
ssize_t firstArcIndex = m_shapes[aPtIndex].first;
const VECTOR2I& newStart = m_arcs[firstArcIndex].GetP0(); //don't ammend the start
const VECTOR2I& newEnd = m_points[aPtIndex - 1];
ammendArc( firstArcIndex, newStart, newEnd );
if( IsSharedPt( aPtIndex ) )
{
m_shapes[aPtIndex].first = m_shapes[aPtIndex].second;
m_shapes[aPtIndex].second = SHAPE_IS_PT;
}
else
{
m_shapes[aPtIndex] = SHAPES_ARE_PT;
}
return;
}
ssize_t currArcIdx = ArcIndex( aPtIndex );
SHAPE_ARC& currentArc = m_arcs[currArcIdx];
SHAPE_ARC newArc1;
SHAPE_ARC newArc2;
VECTOR2I arc1End = ( aCoincident ) ? m_points[aPtIndex] : m_points[aPtIndex - 1];
VECTOR2I arc2Start = m_points[aPtIndex];
newArc1.ConstructFromStartEndCenter( currentArc.GetP0(), arc1End, currentArc.GetCenter(),
currentArc.IsClockwise() );
newArc2.ConstructFromStartEndCenter( arc2Start, currentArc.GetP1(), currentArc.GetCenter(),
currentArc.IsClockwise() );
if( !aCoincident && ArcIndex( aPtIndex - 1 ) != currArcIdx )
{
//Ignore newArc1 as it has zero points
m_arcs[currArcIdx] = newArc2;
}
else
{
m_arcs[currArcIdx] = newArc1;
m_arcs.insert( m_arcs.begin() + currArcIdx + 1, newArc2 );
if( aCoincident )
m_shapes[aPtIndex].second = currArcIdx + 1;
// Only change the arc indices for the second half of the point range
for( int i = aPtIndex; i < PointCount(); i++ )
{
alg::run_on_pair( m_shapes[i], [&]( ssize_t& aIndex ) {
if( aIndex != SHAPE_IS_PT )
aIndex++;
} );
}
}
}
bool SHAPE_LINE_CHAIN_BASE::Collide( const VECTOR2I& aP, int aClearance, int* aActual,
VECTOR2I* aLocation ) const
{
@ -433,36 +524,8 @@ void SHAPE_LINE_CHAIN::Mirror( const SEG& axis )
void SHAPE_LINE_CHAIN::Replace( int aStartIndex, int aEndIndex, const VECTOR2I& aP )
{
if( aEndIndex < 0 )
aEndIndex += PointCount();
if( aStartIndex < 0 )
aStartIndex += PointCount();
aEndIndex = std::min( aEndIndex, PointCount() - 1 );
// N.B. This works because convertArc changes m_shapes on the first run
for( int ind = aStartIndex; ind <= aEndIndex; ind++ )
{
if( m_shapes[ind] != SHAPES_ARE_PT )
alg::run_on_pair( m_shapes[ind], [&]( size_t aIndex )
{
convertArc( aIndex );
} );
}
if( aStartIndex == aEndIndex )
{
m_points[aStartIndex] = aP;
}
else
{
m_points.erase( m_points.begin() + aStartIndex + 1, m_points.begin() + aEndIndex + 1 );
m_points[aStartIndex] = aP;
m_shapes.erase( m_shapes.begin() + aStartIndex + 1, m_shapes.begin() + aEndIndex + 1 );
}
Remove( aStartIndex, aEndIndex );
Insert( aStartIndex, aP );
assert( m_shapes.size() == m_points.size() );
}
@ -481,33 +544,22 @@ void SHAPE_LINE_CHAIN::Replace( int aStartIndex, int aEndIndex, const SHAPE_LINE
SHAPE_LINE_CHAIN newLine = aLine;
// It's possible that the start or end lands on the end of an arc. If so, we'd better have a
// replacement line that matches up to the same coordinates, as we can't break the arc(s).
// @todo fixme: need to handle the case when there are two arcs associated with one point
ssize_t startShape = m_shapes[aStartIndex].first;
ssize_t endShape = m_shapes[aEndIndex].first;
if( startShape >= 0 )
// Remove coincident points in the new line
if( newLine.m_points.front() == m_points[aStartIndex] )
{
wxASSERT( !newLine.PointCount() ||
( newLine.m_points.front() == m_points[aStartIndex] &&
aStartIndex < m_points.size() - 1 ) );
aStartIndex++;
newLine.Remove( 0 );
}
if( endShape >= 0 )
if( newLine.m_points.back() == m_points[aEndIndex] && aEndIndex > 0 )
{
wxASSERT( !newLine.PointCount() ||
( newLine.m_points.back() == m_points[aEndIndex] && aEndIndex > 0 ) );
aEndIndex--;
newLine.Remove( -1 );
}
Remove( aStartIndex, aEndIndex );
if( !aLine.PointCount() )
if( !newLine.PointCount() )
return;
// The total new arcs index is added to the new arc indices
@ -516,14 +568,12 @@ void SHAPE_LINE_CHAIN::Replace( int aStartIndex, int aEndIndex, const SHAPE_LINE
for( std::pair<ssize_t, ssize_t>& shape_pair : new_shapes )
{
if( shape_pair != SHAPES_ARE_PT )
{
if( shape_pair.first != SHAPE_IS_PT )
shape_pair.first += prev_arc_count;
if( shape_pair.second != SHAPE_IS_PT )
shape_pair.second += prev_arc_count;
}
alg::run_on_pair( shape_pair,
[&]( ssize_t& aShape )
{
if( aShape != SHAPE_IS_PT )
aShape += prev_arc_count;
} );
}
m_shapes.insert( m_shapes.begin() + aStartIndex, new_shapes.begin(), new_shapes.end() );
@ -549,6 +599,16 @@ void SHAPE_LINE_CHAIN::Remove( int aStartIndex, int aEndIndex )
return;
aEndIndex = std::min( aEndIndex, PointCount() - 1 );
// Split arcs at start index and end just after the end index
if( aStartIndex < m_points.size() && m_shapes[aStartIndex] != SHAPES_ARE_PT )
splitArc( aStartIndex );
size_t nextIndex = static_cast<size_t>( aEndIndex ) + 1;
if( nextIndex < m_points.size() && m_shapes[aEndIndex] != SHAPES_ARE_PT )
splitArc( nextIndex );
std::set<size_t> extra_arcs;
auto logArcIdxRemoval = [&]( ssize_t& aShapeIndex )
{
@ -975,8 +1035,10 @@ void SHAPE_LINE_CHAIN::Append( const SHAPE_ARC& aArc )
void SHAPE_LINE_CHAIN::Insert( size_t aVertex, const VECTOR2I& aP )
{
if( aVertex < m_points.size() && m_shapes[aVertex] != SHAPES_ARE_PT )
convertArc( aVertex );
wxCHECK( aVertex < m_points.size(), /* void */ );
if( aVertex > 0 && IsPtOnArc( aVertex ) )
splitArc( aVertex );
//@todo need to check we aren't creating duplicate points
m_points.insert( m_points.begin() + aVertex, aP );
@ -988,8 +1050,10 @@ void SHAPE_LINE_CHAIN::Insert( size_t aVertex, const VECTOR2I& aP )
void SHAPE_LINE_CHAIN::Insert( size_t aVertex, const SHAPE_ARC& aArc )
{
if( aVertex < m_points.size() && m_shapes[aVertex] != SHAPES_ARE_PT )
convertArc( aVertex );
wxCHECK( aVertex < m_points.size(), /* void */ );
if( aVertex > 0 && IsPtOnArc( aVertex ) )
splitArc( aVertex );
/// Step 1: Find the position for the new arc in the existing arc vector
ssize_t arc_pos = m_arcs.size();

View File

@ -479,6 +479,64 @@ BOOST_AUTO_TEST_CASE( BasicTTRGeom )
}
/**
* Info to set up an arc start, end and center
*/
struct ARC_START_END_CENTER
{
VECTOR2I m_start;
VECTOR2I m_end;
VECTOR2I m_center;
};
struct ARC_SEC_CASE
{
/// The text context name
std::string m_ctx_name;
/// Geom of the arc
ARC_START_END_CENTER m_geom;
/// clockwise or anti-clockwise?
bool m_clockwise;
/// Expected mid-point of the arc
VECTOR2I m_expected_mid;
};
static const std::vector<ARC_SEC_CASE> arc_sec_cases = {
{ "180 deg, clockwise", { { 100, 0 }, { 0, 0 }, { 50, 0 } }, true, { 50, -50 } },
{ "180 deg, anticlockwise", { { 100, 0 }, { 0, 0 }, { 50, 0 } }, false, { 50, 50 } },
{ "180 deg flipped, clockwise", { { 0, 0 }, { 100, 0 }, { 50, 0 } }, true, { 50, 50 } },
{ "180 deg flipped, anticlockwise", { { 0, 0 }, { 100, 0 }, { 50, 0 } }, false, { 50, -50 } },
{ "90 deg, clockwise", { { -100, 0 }, { 0, 100 }, { 0, 0 } }, true, { -71, 71 } },
{ "90 deg, anticlockwise", { { -100, 0 }, { 0, 100 }, { 0, 0 } }, false, { 71, -71 } },
};
BOOST_AUTO_TEST_CASE( BasicSECGeom )
{
for( const auto& c : arc_sec_cases )
{
BOOST_TEST_CONTEXT( c.m_ctx_name )
{
VECTOR2I start = c.m_geom.m_start;
VECTOR2I end = c.m_geom.m_end;
VECTOR2I center = c.m_geom.m_center;
bool cw = c.m_clockwise;
SHAPE_ARC this_arc;
this_arc.ConstructFromStartEndCenter( start, end, center, cw );
BOOST_CHECK_EQUAL( this_arc.GetArcMid(), c.m_expected_mid );
}
}
}
struct ARC_PT_COLLIDE_CASE
{
std::string m_ctx_name;

View File

@ -1,7 +1,7 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2019 KiCad Developers, see CHANGELOG.TXT for contributors.
* Copyright (C) 2019-2021 KiCad Developers, see CHANGELOG.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
@ -43,23 +43,61 @@ BOOST_AUTO_TEST_CASE( ArcToPolyline )
VECTOR2I( 1500, 0 ),
} );
SHAPE_LINE_CHAIN arc_insert1( SHAPE_ARC( VECTOR2I( 0, -100 ), VECTOR2I( 0, -200 ), 1800 ) );
SHAPE_LINE_CHAIN arc_insert1( SHAPE_ARC( VECTOR2I( 0, -100 ), VECTOR2I( 0, -200 ), 180.0 ) );
SHAPE_LINE_CHAIN arc_insert2( SHAPE_ARC( VECTOR2I( 0, 500 ), VECTOR2I( 0, 400 ), 1800 ) );
SHAPE_LINE_CHAIN arc_insert2( SHAPE_ARC( VECTOR2I( 0, 500 ), VECTOR2I( 0, 400 ), 180.0 ) );
BOOST_CHECK_EQUAL( base_chain.CShapes().size(), base_chain.CPoints().size() );
BOOST_CHECK_EQUAL( arc_insert1.CShapes().size(), arc_insert1.CPoints().size() );
BOOST_CHECK_EQUAL( arc_insert2.CShapes().size(), arc_insert2.CPoints().size() );
base_chain.Insert( 0, SHAPE_ARC( VECTOR2I( 0, -100 ), VECTOR2I( 0, -200 ), 1800 ) );
BOOST_CHECK( GEOM_TEST::IsOutlineValid( base_chain ) );
BOOST_CHECK( GEOM_TEST::IsOutlineValid( arc_insert1 ) );
BOOST_CHECK( GEOM_TEST::IsOutlineValid( arc_insert2 ) );
base_chain.Insert( 0, SHAPE_ARC( VECTOR2I( 0, -100 ), VECTOR2I( 0, -200 ), 1800 ) );
BOOST_CHECK( GEOM_TEST::IsOutlineValid( base_chain ) );
BOOST_CHECK_EQUAL( base_chain.CShapes().size(), base_chain.CPoints().size() );
base_chain.Replace( 0, 2, chain_insert );
BOOST_CHECK( GEOM_TEST::IsOutlineValid( base_chain ) );
BOOST_CHECK_EQUAL( base_chain.CShapes().size(), base_chain.CPoints().size() );
}
// Similar test to above but with larger coordinates, so we have more than one point per arc
BOOST_AUTO_TEST_CASE( ArcToPolylineLargeCoords )
{
SHAPE_LINE_CHAIN base_chain( { VECTOR2I( 0, 0 ), VECTOR2I( 0, 100000 ), VECTOR2I( 100000, 0 ) } );
SHAPE_LINE_CHAIN chain_insert( {
VECTOR2I( 0, 1500000 ),
VECTOR2I( 1500000, 1500000 ),
VECTOR2I( 1500000, 0 ),
} );
base_chain.Append( SHAPE_ARC( VECTOR2I( 200000, 0 ), VECTOR2I( 300000, 100000 ), 180.0 ) );
BOOST_CHECK( GEOM_TEST::IsOutlineValid( base_chain ) );
BOOST_CHECK_EQUAL( base_chain.PointCount(), 11 );
base_chain.Insert( 9, VECTOR2I( 250000, 0 ) );
BOOST_CHECK( GEOM_TEST::IsOutlineValid( base_chain ) );
BOOST_CHECK_EQUAL( base_chain.PointCount(), 12 );
BOOST_CHECK_EQUAL( base_chain.ArcCount(), 2 ); // Should have two arcs after the split
base_chain.Replace( 5, 6, chain_insert );
BOOST_CHECK( GEOM_TEST::IsOutlineValid( base_chain ) );
BOOST_CHECK_EQUAL( base_chain.PointCount(), 13 ); // Adding 3 points, removing 2
BOOST_CHECK_EQUAL( base_chain.ArcCount(), 3 ); // Should have three arcs after the split
base_chain.Replace( 4, 6, VECTOR2I( 550000, 0 ) );
BOOST_CHECK( GEOM_TEST::IsOutlineValid( base_chain ) );
BOOST_CHECK_EQUAL( base_chain.PointCount(), 11 ); // Adding 1 point, removing 3
BOOST_CHECK_EQUAL( base_chain.ArcCount(), 3 ); // Should still have three arcs
}
struct ARC_FOUND_PROPERTIES
{
VECTOR2I m_Start;