ADDED: Pcbnew chamfer and extend tools

Using the new ITEM_MODIFICATION_ROUTINE system, drop in two new
tools: chamfer and line extend. These are two geometric operations
that are relatively common when editing footprints in particular.

Chamfer delegates the geometric calculations to a dedicated unit
in kimath/geometry.
This commit is contained in:
John Beard 2023-07-02 18:59:04 +01:00
parent 8e0e9ce752
commit 046d978ba7
12 changed files with 640 additions and 12 deletions

View File

@ -10,6 +10,7 @@ set( KIMATH_SRCS
src/md5_hash.cpp
src/trigo.cpp
src/geometry/chamfer.cpp
src/geometry/eda_angle.cpp
src/geometry/ellipse.cpp
src/geometry/circle.cpp

View File

@ -0,0 +1,62 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2023 KiCad Developers, see AUTHORS.txt for contributors.
*
* @author Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
*
* 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.
*
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#ifndef __CHAMFER_H
#define __CHAMFER_H
#include <optional>
#include <geometry/seg.h>
/**
* Parameters that define a simple chamfer operation.
*/
struct CHAMFER_PARAMS
{
/// Chamfer set-back distance along the first line
int m_chamfer_setback_a_IU;
/// Chamfer set-back distance along the second line
int m_chamfer_setback_b_IU;
};
struct CHAMFER_RESULT
{
// The chamfer segment
SEG m_chamfer;
// The updated original segments
// These can be empty if the chamfer "consumed" "he original segments
std::optional<SEG> m_updated_seg_a;
std::optional<SEG> m_updated_seg_b;
};
/**
* Compute the chamfer points for a given line pair and chamfer parameters.
*/
std::optional<CHAMFER_RESULT> ComputeChamferPoints( const SEG aSegA, const SEG& aSegB,
const CHAMFER_PARAMS& aChamferParams );
#endif

View File

@ -0,0 +1,107 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2023 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <geometry/chamfer.h>
std::optional<CHAMFER_RESULT> ComputeChamferPoints( const SEG aSegA, const SEG& aSegB,
const CHAMFER_PARAMS& aChamferParams )
{
const int line_a_setback = aChamferParams.m_chamfer_setback_a_IU;
const int line_b_setback = aChamferParams.m_chamfer_setback_b_IU;
if( line_a_setback == 0 && line_b_setback == 0 )
{
// No chamfer to do
// In theory a chamfer of 0 on one side is kind-of valid (adds a collinear point)
// so allow it (using an and above, not an or)
return std::nullopt;
}
if( aSegA.Length() < line_a_setback || aSegB.Length() < line_b_setback )
{
// Chamfer is too big for the line segments
return std::nullopt;
}
// We only support the case where the lines intersect at the end points
// otherwise we would need to decide which inside corner to chamfer
// Figure out which end points are the ones at the intersection
const VECTOR2I* a_pt = nullptr;
const VECTOR2I* b_pt = nullptr;
if( aSegA.A == aSegB.A )
{
a_pt = &aSegA.A;
b_pt = &aSegB.A;
}
else if( aSegA.A == aSegB.B )
{
a_pt = &aSegA.A;
b_pt = &aSegB.B;
}
else if( aSegA.B == aSegB.A )
{
a_pt = &aSegA.B;
b_pt = &aSegB.A;
}
else if( aSegA.B == aSegB.B )
{
a_pt = &aSegA.B;
b_pt = &aSegB.B;
}
if( !a_pt || !b_pt )
{
// No intersection found, so no chamfer to do
return std::nullopt;
}
// These are the other existing line points (the ones that are not the intersection)
const VECTOR2I& a_end_pt = ( &aSegA.A == a_pt ) ? aSegA.B : aSegA.A;
const VECTOR2I& b_end_pt = ( &aSegB.A == b_pt ) ? aSegB.B : aSegB.A;
// Now, construct segment of the set-back lengths, that begins
// at the intersection point and is parallel to each line segments
SEG setback_a( *a_pt, *b_pt + VECTOR2I( a_end_pt - *a_pt ).Resize( line_a_setback ) );
SEG setback_b( *b_pt, *b_pt + VECTOR2I( b_end_pt - *b_pt ).Resize( line_b_setback ) );
// The chamfer segment then goes between the end points of the set-back segments
SEG chamfer( setback_a.B, setback_b.B );
// The adjusted segments go from the old end points to the chamfer ends
std::optional<SEG> new_a;
if( a_end_pt != chamfer.A )
{
new_a = SEG{ a_end_pt, chamfer.A };
}
std::optional<SEG> new_b;
if( b_end_pt != chamfer.B )
{
new_b = SEG{ b_end_pt, chamfer.B };
}
return CHAMFER_RESULT{ chamfer, new_a, new_b };
}

View File

@ -204,10 +204,11 @@ bool EDIT_TOOL::Init()
PCB_ARC_T,
PCB_VIA_T };
static std::vector<KICAD_T> filletTypes = { PCB_SHAPE_LOCATE_POLY_T,
PCB_SHAPE_LOCATE_RECT_T,
PCB_SHAPE_LOCATE_SEGMENT_T };
static std::vector<KICAD_T> filletChamferTypes = { PCB_SHAPE_LOCATE_POLY_T,
PCB_SHAPE_LOCATE_RECT_T,
PCB_SHAPE_LOCATE_SEGMENT_T };
static std::vector<KICAD_T> lineExtendTypes = { PCB_SHAPE_LOCATE_SEGMENT_T };
// Add context menu entries that are displayed when selection tool is active
CONDITIONAL_MENU& menu = m_selectionTool->GetToolMenu().GetMenu();
@ -229,7 +230,10 @@ bool EDIT_TOOL::Init()
&& SELECTION_CONDITIONS::OnlyTypes( GENERAL_COLLECTOR::DraggableItems )
&& !SELECTION_CONDITIONS::OnlyTypes( { PCB_FOOTPRINT_T } ) );
menu.AddItem( PCB_ACTIONS::filletTracks, SELECTION_CONDITIONS::OnlyTypes( trackTypes ) );
menu.AddItem( PCB_ACTIONS::filletLines, SELECTION_CONDITIONS::OnlyTypes( filletTypes ) );
menu.AddItem( PCB_ACTIONS::filletLines, SELECTION_CONDITIONS::OnlyTypes( filletChamferTypes ) );
menu.AddItem( PCB_ACTIONS::chamferLines, SELECTION_CONDITIONS::OnlyTypes( filletChamferTypes ) );
menu.AddItem( PCB_ACTIONS::extendLines, SELECTION_CONDITIONS::OnlyTypes( lineExtendTypes )
&& SELECTION_CONDITIONS::Count( 2 ) );
menu.AddItem( PCB_ACTIONS::rotateCcw, SELECTION_CONDITIONS::NotEmpty );
menu.AddItem( PCB_ACTIONS::rotateCw, SELECTION_CONDITIONS::NotEmpty );
menu.AddItem( PCB_ACTIONS::flip, SELECTION_CONDITIONS::NotEmpty );
@ -1048,8 +1052,45 @@ static std::optional<int> GetFilletParams( PCB_BASE_EDIT_FRAME& aFrame, wxString
return filletRadiusIU;
}
/**
* Prompt the user for chamfer parameters
*
* @param aFrame
* @param aErrorMsg filled with an error message if the parameter is invalid somehow
* @return std::optional<int> the chamfer parameters or std::nullopt if no
* valid fillet specified
*/
static std::optional<CHAMFER_PARAMS> GetChamferParams( PCB_BASE_EDIT_FRAME& aFrame,
wxString& aErrorMsg )
{
// Non-zero and the KLC default for Fab layer chamfers
const int default_setback = pcbIUScale.mmToIU( 1 );
// Store last used setback to allow pressing "enter" if repeat chamfer is required
static CHAMFER_PARAMS params{ default_setback, default_setback };
int EDIT_TOOL::FilletLines( const TOOL_EVENT& aEvent )
WX_UNIT_ENTRY_DIALOG dia( &aFrame, _( "Enter chamfer setback:" ), _( "Chamfer Lines" ),
params.m_chamfer_setback_a_IU );
if( dia.ShowModal() == wxID_CANCEL )
return std::nullopt;
params.m_chamfer_setback_a_IU = dia.GetValue();
// It's hard to easily specify an asymmetric chamfer (which line gets the longer
// setbeck?), so we just use the same setback for each
params.m_chamfer_setback_b_IU = params.m_chamfer_setback_a_IU;
// Some technically-valid chamfers are not useful to actually do
if( params.m_chamfer_setback_a_IU == 0 )
{
aErrorMsg = _( "A setback of zero was entered.\n"
"The chamfer operation was not performed." );
return std::nullopt;
}
return params;
}
int EDIT_TOOL::ModifyLines( const TOOL_EVENT& aEvent )
{
PCB_SELECTION& selection = m_selectionTool->RequestSelection(
[]( const VECTOR2I& aPt, GENERAL_COLLECTOR& aCollector, PCB_SELECTION_TOOL* sTool )
@ -1183,6 +1224,30 @@ int EDIT_TOOL::FilletLines( const TOOL_EVENT& aEvent )
*filletRadiusIU );
}
}
else if( aEvent.IsAction( &PCB_ACTIONS::chamferLines ) )
{
const std::optional<CHAMFER_PARAMS> chamfer_params =
GetChamferParams( *frame(), error_message );
if( chamfer_params.has_value() )
{
pairwise_line_routine = std::make_unique<LINE_CHAMFER_ROUTINE>(
frame()->GetModel(), item_creation_handler, item_modification_handler,
*chamfer_params );
}
}
else if( aEvent.IsAction( &PCB_ACTIONS::extendLines ) )
{
if( selection.CountType( PCB_SHAPE_LOCATE_SEGMENT_T ) != 2 )
{
error_message = _( "Exactly two lines must be selected to extend them." );
}
else
{
pairwise_line_routine = std::make_unique<LINE_EXTENSION_ROUTINE>(
frame()->GetModel(), item_creation_handler, item_modification_handler );
}
}
if( !pairwise_line_routine )
{
@ -2493,7 +2558,9 @@ void EDIT_TOOL::setTransitions()
Go( &EDIT_TOOL::PackAndMoveFootprints, PCB_ACTIONS::packAndMoveFootprints.MakeEvent() );
Go( &EDIT_TOOL::ChangeTrackWidth, PCB_ACTIONS::changeTrackWidth.MakeEvent() );
Go( &EDIT_TOOL::FilletTracks, PCB_ACTIONS::filletTracks.MakeEvent() );
Go( &EDIT_TOOL::FilletLines, PCB_ACTIONS::filletLines.MakeEvent() );
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::filletLines.MakeEvent() );
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::chamferLines.MakeEvent() );
Go( &EDIT_TOOL::ModifyLines, PCB_ACTIONS::extendLines.MakeEvent() );
Go( &EDIT_TOOL::copyToClipboard, ACTIONS::copy.MakeEvent() );
Go( &EDIT_TOOL::copyToClipboard, PCB_ACTIONS::copyWithReference.MakeEvent() );

View File

@ -121,9 +121,10 @@ public:
int FilletTracks( const TOOL_EVENT& aEvent );
/**
* Fillet (i.e. adds an arc tangent to) all selected straight lines by a user defined radius.
* "Modify" graphical lines. This includes operations such as filleting, chamfering,
* extending to meet.
*/
int FilletLines( const TOOL_EVENT& aEvent );
int ModifyLines( const TOOL_EVENT& aEvent );
/**
* Delete currently selected items.

View File

@ -23,6 +23,18 @@
#include "item_modification_routine.h"
namespace
{
/**
* Check if two segments share an endpoint (can be at either end of either segment)
*/
bool SegmentsShareEndpoint( const SEG& aSegA, const SEG& aSegB )
{
return ( aSegA.A == aSegB.A || aSegA.A == aSegB.B || aSegA.B == aSegB.A || aSegA.B == aSegB.B );
}
} // namespace
wxString LINE_FILLET_ROUTINE::GetCommitDescription() const
{
@ -130,3 +142,129 @@ void LINE_FILLET_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB
AddSuccess();
}
wxString LINE_CHAMFER_ROUTINE::GetCommitDescription() const
{
return _( "Chamfer Lines" );
}
wxString LINE_CHAMFER_ROUTINE::GetCompleteFailureMessage() const
{
return _( "Unable to chamfer the selected lines." );
}
wxString LINE_CHAMFER_ROUTINE::GetSomeFailuresMessage() const
{
return _( "Some of the lines could not be chamfered." );
}
void LINE_CHAMFER_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB )
{
if( aLineA.GetLength() == 0.0 || aLineB.GetLength() == 0.0 )
return;
SEG seg_a( aLineA.GetStart(), aLineA.GetEnd() );
SEG seg_b( aLineB.GetStart(), aLineB.GetEnd() );
// If the segments share an endpoint, we won't try to chamfer them
// (we could extend to the intersection point, but this gets complicated
// and inconsistent when you select more than two lines)
if( !SegmentsShareEndpoint( seg_a, seg_b ) )
{
// not an error, lots of lines in a 2+ line selection will not intersect
return;
}
std::optional<CHAMFER_RESULT> chamfer_result =
ComputeChamferPoints( seg_a, seg_b, m_chamferParams );
if( !chamfer_result )
{
AddFailure();
return;
}
auto tSegment = std::make_unique<PCB_SHAPE>( GetBoard(), SHAPE_T::SEGMENT );
tSegment->SetStart( chamfer_result->m_chamfer.A );
tSegment->SetEnd( chamfer_result->m_chamfer.B );
// Copy properties from one of the source lines
tSegment->SetWidth( aLineA.GetWidth() );
tSegment->SetLayer( aLineA.GetLayer() );
tSegment->SetLocked( aLineA.IsLocked() );
AddNewItem( std::move( tSegment ) );
MarkItemModified( aLineA );
MarkItemModified( aLineB );
// Shorten the original lines
aLineA.SetStart( chamfer_result->m_updated_seg_a->A );
aLineA.SetEnd( chamfer_result->m_updated_seg_a->B );
aLineB.SetStart( chamfer_result->m_updated_seg_b->A );
aLineB.SetEnd( chamfer_result->m_updated_seg_b->B );
AddSuccess();
}
wxString LINE_EXTENSION_ROUTINE::GetCommitDescription() const
{
return _( "Extend Lines to Meet" );
}
wxString LINE_EXTENSION_ROUTINE::GetCompleteFailureMessage() const
{
return _( "Unable to extend the selected lines to meet." );
}
wxString LINE_EXTENSION_ROUTINE::GetSomeFailuresMessage() const
{
return _( "Some of the lines could not be extended to meet." );
}
void LINE_EXTENSION_ROUTINE::ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB )
{
if( aLineA.GetLength() == 0.0 || aLineB.GetLength() == 0.0 )
return;
SEG seg_a( aLineA.GetStart(), aLineA.GetEnd() );
SEG seg_b( aLineB.GetStart(), aLineB.GetEnd() );
if( seg_a.Intersects( seg_b ) )
{
// already intersecting, nothing to do
return;
}
OPT_VECTOR2I intersection = seg_a.IntersectLines( seg_b );
if( !intersection )
{
// This might be an error, but it's also possible that the lines are
// parallel and don't intersect. We'll just ignore this case.
return;
}
const auto line_extender = [&]( const SEG& aSeg, PCB_SHAPE& aLine )
{
// If the intersection point is not already n the line, we'll extend to it
if( !aSeg.Contains( *intersection ) )
{
const int dist_start = ( *intersection - aSeg.A ).EuclideanNorm();
const int dist_end = ( *intersection - aSeg.B ).EuclideanNorm();
const VECTOR2I& furthest_pt = ( dist_start < dist_end ) ? aSeg.B : aSeg.A;
MarkItemModified( aLine );
aLine.SetStart( furthest_pt );
aLine.SetEnd( *intersection );
}
};
line_extender( seg_a, aLineA );
line_extender( seg_b, aLineB );
AddSuccess();
}

View File

@ -32,6 +32,8 @@
#include <board_item.h>
#include <pcb_shape.h>
#include <geometry/chamfer.h>
/**
* @brief An object that has the ability to modify items on a board
*
@ -47,7 +49,7 @@ public:
/*
* Handlers for receiving changes from the tool
*
* These is used to allow the tool's caller to make changes to
* These are used to allow the tool's caller to make changes to
* affected board items using extra information that the tool
* does not have access to (e.g. is this an FP editor, was
* the line created from a rectangle and needs to be added, not
@ -55,11 +57,20 @@ public:
*
* We can't store them up until the end, because modifications
* need the old state to be known.
*/
/**
* Handler for creating a new item on the board
*
* @param PCB_SHAPE& the line to modify
* @param bool true if the shape was created, false if it was only modified
* @param PCB_SHAPE& the shape to add
*/
using CREATION_HANDLER = std::function<void( std::unique_ptr<PCB_SHAPE> )>;
/**
* Handler for modifying an existing item on the board
*
* @param PCB_SHAPE& the shape to modify
*/
using MODIFICATION_HANDLER = std::function<void( PCB_SHAPE& )>;
ITEM_MODIFICATION_ROUTINE( BOARD_ITEM* aBoard, CREATION_HANDLER aCreationHandler,
@ -171,4 +182,47 @@ private:
int m_filletRadiusIU;
};
/**
* Pairwise line tool that adds a chamfer between the lines.
*/
class LINE_CHAMFER_ROUTINE : public PAIRWISE_LINE_ROUTINE
{
public:
LINE_CHAMFER_ROUTINE( BOARD_ITEM* aBoard, CREATION_HANDLER aCreationHandler,
MODIFICATION_HANDLER aModificationHandler,
CHAMFER_PARAMS aChamferParams ) :
PAIRWISE_LINE_ROUTINE( aBoard, aCreationHandler, aModificationHandler ),
m_chamferParams( std::move( aChamferParams ) )
{
}
wxString GetCommitDescription() const override;
wxString GetCompleteFailureMessage() const override;
wxString GetSomeFailuresMessage() const override;
void ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB ) override;
private:
const CHAMFER_PARAMS m_chamferParams;
};
/**
* Pairwise extend to corner or meeting tool
*/
class LINE_EXTENSION_ROUTINE : public PAIRWISE_LINE_ROUTINE
{
public:
LINE_EXTENSION_ROUTINE( BOARD_ITEM* aBoard, CREATION_HANDLER aCreationHandler,
MODIFICATION_HANDLER aModificationHandler ) :
PAIRWISE_LINE_ROUTINE( aBoard, aCreationHandler, aModificationHandler )
{
}
wxString GetCommitDescription() const override;
wxString GetCompleteFailureMessage() const override;
wxString GetSomeFailuresMessage() const override;
void ProcessLinePair( PCB_SHAPE& aLineA, PCB_SHAPE& aLineB ) override;
};
#endif /* ITEM_MODIFICATION_ROUTINE_H_ */

View File

@ -403,6 +403,16 @@ TOOL_ACTION PCB_ACTIONS::filletLines( "pcbnew.InteractiveEdit.filletLines",
AS_GLOBAL, 0, "",
_( "Fillet Lines" ), _( "Adds arcs tangent to the selected lines" ) );
TOOL_ACTION PCB_ACTIONS::chamferLines( "pcbnew.InteractiveEdit.chamferLines",
AS_GLOBAL, 0, "",
_( "Chamfer Lines" ),
_( "Cut away corners between selected lines" ) );
TOOL_ACTION PCB_ACTIONS::extendLines( "pcbnew.InteractiveEdit.extendLines",
AS_GLOBAL, 0, "",
_( "Extend Lines to Meet" ),
_( "Extend lines to meet each other" ) );
TOOL_ACTION PCB_ACTIONS::deleteFull( TOOL_ACTION_ARGS()
.Name( "pcbnew.InteractiveEdit.deleteFull" )
.Scope( AS_GLOBAL )

View File

@ -152,7 +152,13 @@ public:
/// Fillet (i.e. adds an arc tangent to) all selected straight tracks by a user defined radius
static TOOL_ACTION filletTracks;
/// Fillet (i.e. adds an arc tangent to) all selected straight lines by a user defined radius
static TOOL_ACTION filletLines;
/// Chamfer (i.e. adds a straight line) all selected straight lines by a user defined setback
static TOOL_ACTION chamferLines;
/// Extend selected lines to meet at a point
static TOOL_ACTION extendLines;
/// Activation of the edit tool
static TOOL_ACTION properties;

View File

@ -27,6 +27,7 @@ set( QA_KIMATH_SRCS
test_kimath.cpp
geometry/test_chamfer.cpp
geometry/test_eda_angle.cpp
geometry/test_ellipse_to_bezier.cpp
geometry/test_fillet.cpp

View File

@ -313,6 +313,17 @@ inline bool IsPolySetValid( const SHAPE_POLY_SET& aSet )
return true;
}
/**
* @brief Check that two SEGs have the same end points, in either order
*
* That is to say SEG(A, B) == SEG(A, B), but also SEG(A, B) == SEG(B, A)
*/
inline bool SegmentsHaveSameEndPoints( const SEG& aSeg1, const SEG& aSeg2 )
{
return ( aSeg1.A == aSeg2.A && aSeg1.B == aSeg2.B )
|| ( aSeg1.A == aSeg2.B && aSeg1.B == aSeg2.A );
}
} // namespace GEOM_TEST
namespace BOOST_TEST_PRINT_NAMESPACE_OPEN

View File

@ -0,0 +1,170 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2018 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <boost/test/unit_test.hpp>
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <geometry/chamfer.h>
#include "geom_test_utils.h"
struct ChamferFixture
{
};
/**
* Declares the FilletFixture struct as the boost test fixture.
*/
BOOST_FIXTURE_TEST_SUITE( Chamfer, ChamferFixture )
struct TWO_LINE_CHAMFER_TEST_CASE
{
SEG m_seg_a;
SEG m_seg_b;
CHAMFER_PARAMS m_params;
std::optional<CHAMFER_RESULT> m_expected_result;
};
static void DoChamferTestChecks( const TWO_LINE_CHAMFER_TEST_CASE& aTestCase )
{
// Actally do the chamfer
const std::optional<CHAMFER_RESULT> chamfer_result =
ComputeChamferPoints( aTestCase.m_seg_a, aTestCase.m_seg_b, aTestCase.m_params );
BOOST_REQUIRE_EQUAL( chamfer_result.has_value(), aTestCase.m_expected_result.has_value() );
if( chamfer_result.has_value() )
{
const CHAMFER_RESULT& expected_result = aTestCase.m_expected_result.value();
const CHAMFER_RESULT& actual_result = chamfer_result.value();
BOOST_CHECK_PREDICATE( GEOM_TEST::SegmentsHaveSameEndPoints,
( actual_result.m_chamfer )( expected_result.m_chamfer ) );
const auto check_updated_seg =
[&]( const std::optional<SEG>& updated_seg, const std::optional<SEG>& expected_seg )
{
BOOST_REQUIRE_EQUAL( updated_seg.has_value(), expected_seg.has_value() );
if( updated_seg.has_value() )
{
BOOST_CHECK_PREDICATE( GEOM_TEST::SegmentsHaveSameEndPoints,
( *updated_seg )( *expected_seg ) );
}
};
check_updated_seg( actual_result.m_updated_seg_a, expected_result.m_updated_seg_a );
check_updated_seg( actual_result.m_updated_seg_b, expected_result.m_updated_seg_b );
}
}
BOOST_AUTO_TEST_CASE( SimpleChamferAtOrigin )
{
/* 10
* 0,0 +----+-------------> 1000
* | /
* | /
* 10 +
* |
* v 1000
* */
const TWO_LINE_CHAMFER_TEST_CASE testcase{
{ VECTOR2I( 0, 0 ), VECTOR2I( 1000, 0 ) },
{ VECTOR2I( 0, 0 ), VECTOR2I( 0, 1000 ) },
{ 10, 10 },
{ {
SEG( VECTOR2I( 10, 0 ), VECTOR2I( 0, 10 ) ), // chamfer
{ SEG( VECTOR2I( 10, 0 ), VECTOR2I( 1000, 0 ) ) }, // rest of the line A
{ SEG( VECTOR2I( 0, 10 ), VECTOR2I( 0, 1000 ) ) }, // rest of the line B
} },
};
DoChamferTestChecks( testcase );
}
BOOST_AUTO_TEST_CASE( SimpleChamferNotAtOrigin )
{
// Same as above but the intersection is not at the origin
const TWO_LINE_CHAMFER_TEST_CASE testcase{
{ VECTOR2I( 1000, 1000 ), VECTOR2I( 2000, 1000 ) },
{ VECTOR2I( 1000, 1000 ), VECTOR2I( 1000, 2000 ) },
{ 10, 10 },
{ {
SEG( VECTOR2I( 1010, 1000 ), VECTOR2I( 1000, 1010 ) ),
{ SEG( VECTOR2I( 1010, 1000 ), VECTOR2I( 2000, 1000 ) ) },
{ SEG( VECTOR2I( 1000, 1010 ), VECTOR2I( 1000, 2000 ) ) },
} },
};
DoChamferTestChecks( testcase );
}
BOOST_AUTO_TEST_CASE( AsymmetricChamfer )
{
// Same as above but the intersection is not at the origin
const TWO_LINE_CHAMFER_TEST_CASE testcase{
{ VECTOR2I( 0, 0 ), VECTOR2I( 1000, 0 ) },
{ VECTOR2I( 0, 0 ), VECTOR2I( 0, 1000 ) },
{ 10, 100 },
{ {
SEG( VECTOR2I( 10, 0 ), VECTOR2I( 0, 100 ) ), // chamfer
{ SEG( VECTOR2I( 10, 0 ), VECTOR2I( 1000, 0 ) ) }, // rest of the line A
{ SEG( VECTOR2I( 0, 100 ), VECTOR2I( 0, 1000 ) ) }, // rest of the line B
} },
};
DoChamferTestChecks( testcase );
}
BOOST_AUTO_TEST_CASE( ChamferFullLength )
{
// Chamfer consumes the entire length of a line
const TWO_LINE_CHAMFER_TEST_CASE testcase{
{ VECTOR2I( 0, 0 ), VECTOR2I( 1000, 0 ) },
{ VECTOR2I( 0, 0 ), VECTOR2I( 0, 100 ) },
{ 100, 100 },
{ {
SEG( VECTOR2I( 100, 0 ), VECTOR2I( 0, 100 ) ), // chamfer
{ SEG( VECTOR2I( 100, 0 ), VECTOR2I( 1000, 0 ) ) }, // rest of the line A
std::nullopt, // line b no longer exists
} },
};
DoChamferTestChecks( testcase );
}
BOOST_AUTO_TEST_CASE( ChamferOverFullLength )
{
// Chamfer consumes the entire length of a line
const TWO_LINE_CHAMFER_TEST_CASE testcase{
{ VECTOR2I( 0, 0 ), VECTOR2I( 1000, 0 ) },
{ VECTOR2I( 0, 0 ), VECTOR2I( 0, 100 ) },
{ 150, 150 }, // > 100
std::nullopt,
};
DoChamferTestChecks( testcase );
}
BOOST_AUTO_TEST_SUITE_END()