Browse Source

Snapping: Add construction geometry snapping

This is a pretty major rework of the snapping system.
The GRID_HELPERs now have a separate CONSTRUCTION_MANAGER
which handles some of the state involving "construction
geometry".

This is fed with 'extended' geometry (e.g. "infinite" lines from
segments) for use in generating things like intersection points.
It also handles adding this geoemtry to a GAL view item
(CONSTRUCTION_GEOM) for display to the user.

The process is:

* A TOOL creates a GRID_HELPER
* Optionally, it pre-loads a "persistent" batch of construction
  geometry (e.g. for an item's original position)
* The grid helper finds useful snap 'anchors' as before, including
  those involving the construction items.
* Other items on the board can be 'activated' by snapping to one
  of their main points. Then, if it has construction geometry,
  it will be added to the display. At most 2 items of this kind of
  geometry are shown, plus the original item, to reduce avoid
  too much clutter.

The dashed snap lines state machine is also handled in the
CONSTRUCTION_MANAGER and displayed in the CONSTRUCTION_GEOM item.
jobs
John Beard 1 year ago
parent
commit
b2be0d39bd
  1. 5
      common/CMakeLists.txt
  2. 161
      common/preview_items/construction_geom.cpp
  3. 76
      common/preview_items/item_drawing_utils.cpp
  4. 32
      common/preview_items/snap_indicator.cpp
  5. 300
      common/tool/construction_manager.cpp
  6. 54
      common/tool/grid_helper.cpp
  7. 65
      eeschema/tools/ee_grid_helper.cpp
  8. 84
      include/preview_items/construction_geom.h
  9. 57
      include/preview_items/item_drawing_utils.h
  10. 179
      include/tool/construction_manager.h
  11. 48
      include/tool/grid_helper.h
  12. 6
      libs/kimath/CMakeLists.txt
  13. 2
      libs/kimath/include/geometry/circle.h
  14. 95
      libs/kimath/include/geometry/half_line.h
  15. 17
      libs/kimath/include/geometry/intersection.h
  16. 67
      libs/kimath/include/geometry/line.h
  17. 59
      libs/kimath/include/geometry/nearest.h
  18. 12
      libs/kimath/include/geometry/point_types.h
  19. 72
      libs/kimath/include/geometry/shape_utils.h
  20. 3
      libs/kimath/include/geometry/vector_utils.h
  21. 5
      libs/kimath/include/math/box2.h
  22. 13
      libs/kimath/include/math/vector2d.h
  23. 131
      libs/kimath/src/geometry/half_line.cpp
  24. 221
      libs/kimath/src/geometry/intersection.cpp
  25. 70
      libs/kimath/src/geometry/line.cpp
  26. 114
      libs/kimath/src/geometry/nearest.cpp
  27. 141
      libs/kimath/src/geometry/shape_utils.cpp
  28. 677
      pcbnew/tools/pcb_grid_helper.cpp
  29. 40
      pcbnew/tools/pcb_grid_helper.h
  30. 11
      pcbnew/tools/pcb_point_editor.cpp
  31. 3
      qa/tests/libs/kimath/CMakeLists.txt
  32. 8
      qa/tests/libs/kimath/geometry/geom_test_utils.cpp
  33. 1
      qa/tests/libs/kimath/geometry/geom_test_utils.h
  34. 193
      qa/tests/libs/kimath/geometry/test_half_line.cpp
  35. 32
      qa/tests/libs/kimath/math/test_box2.cpp

5
common/CMakeLists.txt

@ -457,7 +457,9 @@ set( COMMON_PREVIEW_ITEMS_SRCS
preview_items/arc_assistant.cpp
preview_items/arc_geom_manager.cpp
preview_items/centreline_rect_item.cpp
preview_items/construction_geom.cpp
preview_items/draw_context.cpp
preview_items/item_drawing_utils.cpp
preview_items/polygon_geom_manager.cpp
preview_items/polygon_item.cpp
preview_items/preview_utils.cpp
@ -603,7 +605,7 @@ set( COMMON_SRCS
tool/action_toolbar.cpp
tool/actions.cpp
tool/common_control.cpp
tool/library_editor_control.cpp
tool/construction_manager.cpp
tool/common_tools.cpp
tool/conditional_menu.cpp
tool/edit_constraints.cpp
@ -612,6 +614,7 @@ set( COMMON_SRCS
tool/embed_tool.cpp
tool/grid_helper.cpp
tool/grid_menu.cpp
tool/library_editor_control.cpp
tool/picker_tool.cpp
tool/properties_tool.cpp
tool/selection.cpp

161
common/preview_items/construction_geom.cpp

@ -0,0 +1,161 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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 "preview_items/construction_geom.h"
#include <layer_ids.h>
#include <gal/graphics_abstraction_layer.h>
#include <geometry/shape_utils.h>
#include <preview_items/item_drawing_utils.h>
#include <view/view.h>
using namespace KIGFX;
CONSTRUCTION_GEOM::CONSTRUCTION_GEOM() :
EDA_ITEM( nullptr, NOT_USED ), // Never added to a BOARD/SCHEMATIC so it needs no type
m_color( COLOR4D::WHITE ), m_persistentColor( COLOR4D::WHITE )
{
}
void CONSTRUCTION_GEOM::AddDrawable( const DRAWABLE& aItem, bool aPersistent )
{
m_drawables.push_back( { aItem, aPersistent } );
}
void CONSTRUCTION_GEOM::ClearDrawables()
{
m_drawables.clear();
}
const BOX2I CONSTRUCTION_GEOM::ViewBBox() const
{
// We could be a bit more circumspect here, but much of the time the
// enxtended lines go across the whole screen anyway
BOX2I bbox;
bbox.SetMaximum();
return bbox;
}
void CONSTRUCTION_GEOM::ViewDraw( int aLayer, VIEW* aView ) const
{
GAL& gal = *aView->GetGAL();
gal.SetIsFill( false );
gal.SetIsStroke( true );
gal.SetLineWidth( 1 );
BOX2D viewportD = aView->GetViewport();
BOX2I viewport( VECTOR2I( viewportD.GetPosition() ), VECTOR2I( viewportD.GetSize() ) );
const bool haveSnapLine = m_snapLine && m_snapLine->Length() != 0;
// Avoid fighting with the snap line
const auto drawLineIfNotAlsoSnapLine = [&]( const SEG& aLine )
{
if( !haveSnapLine || !aLine.ApproxCollinear( *m_snapLine, 1 ) )
{
gal.DrawLine( aLine.A, aLine.B );
}
};
// Draw all the items
for( const DRAWABLE_INFO& drawable : m_drawables )
{
gal.SetStrokeColor( drawable.IsPersistent ? m_persistentColor : m_color );
std::visit(
[&]( const auto& visited )
{
using ItemType = std::decay_t<decltype( visited )>;
if constexpr( std::is_same_v<ItemType, LINE> )
{
// Extend the segment to the viewport boundary
std::optional<SEG> segToBoundary =
KIGEOM::ClipLineToBox( visited, viewport );
if( segToBoundary )
{
drawLineIfNotAlsoSnapLine( *segToBoundary );
}
}
else if constexpr( std::is_same_v<ItemType, HALF_LINE> )
{
// Extend the ray to the viewport boundary
std::optional<SEG> segToBoundary =
KIGEOM::ClipHalfLineToBox( visited, viewport );
if( segToBoundary )
{
drawLineIfNotAlsoSnapLine( *segToBoundary );
}
}
else if constexpr( std::is_same_v<ItemType, SEG> )
{
drawLineIfNotAlsoSnapLine( visited );
}
else if constexpr( std::is_same_v<ItemType, CIRCLE> )
{
gal.DrawCircle( visited.Center, visited.Radius );
}
else if constexpr( std::is_same_v<ItemType, SHAPE_ARC> )
{
gal.DrawArc( visited.GetCenter(), visited.GetRadius(),
visited.GetStartAngle(), visited.GetCentralAngle() );
}
else if constexpr( std::is_same_v<ItemType, VECTOR2I> )
{
KIGFX::DrawCross( gal, visited, aView->ToWorld( 16 ) );
}
},
drawable.Item );
}
if( haveSnapLine )
{
gal.SetStrokeColor( m_persistentColor );
const int dashSizeBasis = aView->ToWorld( 12 );
const int snapOriginMarkerSize = aView->ToWorld( 16 );
// Avoid clash with the snap marker if very close
const int omitStartMarkerIfWithinLength = aView->ToWorld( 8 );
// The line itself
KIGFX::DrawDashedLine( gal, *m_snapLine, dashSizeBasis );
// The line origin marker if the line is long enough
if( m_snapLine->A.Distance( m_snapLine->B ) > omitStartMarkerIfWithinLength )
{
KIGFX::DrawCross( gal, m_snapLine->A, snapOriginMarkerSize );
gal.DrawCircle( m_snapLine->A, snapOriginMarkerSize / 2 );
}
}
}
void CONSTRUCTION_GEOM::ViewGetLayers( int aLayers[], int& aCount ) const
{
aLayers[0] = LAYER_GP_OVERLAY;
aCount = 1;
}

76
common/preview_items/item_drawing_utils.cpp

@ -0,0 +1,76 @@
/*
* This program source code file is part of KICAD, a free EDA CAD application.
*
* Copyright (C) 2024 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 "preview_items/item_drawing_utils.h"
#include <array>
#include <geometry/geometry_utils.h>
#include <geometry/seg.h>
using namespace KIGFX;
void KIGFX::DrawCross( GAL& aGal, const VECTOR2I& aPosition, int aSize )
{
const int size = aSize / 2;
aGal.DrawLine( aPosition - VECTOR2I( size, 0 ), aPosition + VECTOR2I( size, 0 ) );
aGal.DrawLine( aPosition - VECTOR2I( 0, size ), aPosition + VECTOR2I( 0, size ) );
}
void KIGFX::DrawDashedLine( GAL& aGal, const SEG& aSeg, double aDashSize )
{
const std::array<double, 2> strokes = { aDashSize, aDashSize / 2 };
const double dashCycleLen = strokes[0] + strokes[1];
// The dash cycle length must be at least 1 pixel.
wxASSERT( dashCycleLen * aGal.GetWorldScale() > 1 );
const BOX2I clip = BOX2I::ByCorners( aSeg.A, aSeg.B );
const double theta = atan2( aSeg.B.y - aSeg.A.y, aSeg.B.x - aSeg.A.x );
const VECTOR2D cycleVec{
dashCycleLen * cos( theta ),
dashCycleLen * sin( theta ),
};
const VECTOR2D dashVec{
strokes[0] * cos( theta ),
strokes[0] * sin( theta ),
};
unsigned cyclei = 0;
while( true )
{
const VECTOR2D dashStart = aSeg.A + cycleVec * cyclei;
const VECTOR2D dashEnd = dashStart + dashVec;
// Drawing each segment can be done rounded to ints.
SEG dashSeg{ KiROUND( dashStart ), KiROUND( dashEnd ) };
if( ClipLine( &clip, dashSeg.A.x, dashSeg.A.y, dashSeg.B.x, dashSeg.B.y ) )
break;
aGal.DrawLine( dashSeg.A, dashSeg.B );
++cyclei;
}
}

32
common/preview_items/snap_indicator.cpp

@ -1,7 +1,7 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 20204 KiCad Developers, see AUTHORS.txt for contributors.
* Copyright (C) 2024 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
@ -21,7 +21,7 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <preview_items/snap_indicator.h>
#include "preview_items/snap_indicator.h"
#include <gal/graphics_abstraction_layer.h>
@ -124,6 +124,16 @@ static void DrawIntersectionIcon( GAL& aGal, const VECTOR2I& aPosition, int aSiz
aGal.DrawLine( aPosition - xLeg, aPosition + xLeg );
}
static void DrawOnElementIcon( GAL& aGal, const VECTOR2I& aPosition, int aSize )
{
const int nodeRadius = aSize / 8;
// A bit like midpoint by off to one side
DrawSnapNode( aGal, aPosition + VECTOR2I( aSize / 4, 0 ), nodeRadius );
aGal.DrawLine( aPosition - VECTOR2I( aSize / 2, 0 ), aPosition + VECTOR2I( aSize / 2, 0 ) );
}
void SNAP_INDICATOR::ViewDraw( int, VIEW* aView ) const
{
@ -134,19 +144,9 @@ void SNAP_INDICATOR::ViewDraw( int, VIEW* aView ) const
gal.SetFillColor( m_color );
const auto scaleSize = [&]( double aSize ) -> int
{
return aView->ToWorld( aSize );
};
const auto scaleVec = [&]( const VECTOR2I& aVec ) -> VECTOR2I
{
return aView->ToWorld( aVec, false );
};
// Put the icon near the x-line, so it doesn't overlap with the ruler helpers
const VECTOR2I typeIconPos = m_position + scaleVec( { 24, 10 } );
const int size = scaleSize( 16 );
const VECTOR2I typeIconPos = m_position + aView->ToWorld( { 24, 10 }, false );
const int size = aView->ToWorld( 16 );
// For now, choose the first type that is set
if( m_snapTypes & POINT_TYPE::PT_CORNER )
@ -173,4 +173,8 @@ void SNAP_INDICATOR::ViewDraw( int, VIEW* aView ) const
{
DrawIntersectionIcon( gal, typeIconPos, size );
}
else if( m_snapTypes & POINT_TYPE::PT_ON_ELEMENT )
{
DrawOnElementIcon( gal, typeIconPos, size );
}
}

300
common/tool/construction_manager.cpp

@ -0,0 +1,300 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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 <tool/construction_manager.h>
CONSTRUCTION_MANAGER::CONSTRUCTION_MANAGER( KIGFX::CONSTRUCTION_GEOM& aHelper ) :
m_constructionGeomPreview( aHelper )
{
}
void CONSTRUCTION_MANAGER::updateView()
{
if( m_updateCallback )
{
bool showAnything = m_persistentConstructionBatch || !m_temporaryConstructionBatches.empty()
|| ( m_snapLineOrigin && m_snapLineEnd );
m_updateCallback( showAnything );
}
}
void CONSTRUCTION_MANAGER::AddConstructionItems( CONSTRUCTION_ITEM_BATCH aBatch,
bool aIsPersistent )
{
if( aIsPersistent )
{
// We only keep one previous persistent batch for the moment
m_persistentConstructionBatch = std::move( aBatch );
}
else
{
bool anyNewItems = false;
for( CONSTRUCTION_ITEM& item : aBatch )
{
if( m_involvedItems.count( item.Item ) == 0 )
{
anyNewItems = true;
break;
}
}
// If there are no new items involved, don't bother adding the batch
if( !anyNewItems )
{
return;
}
// We only keep up to one previous temporary batch and the current one
// we could make this a setting if we want to keep more, but it gets cluttered
const int maxTempItems = 2;
while( m_temporaryConstructionBatches.size() >= maxTempItems )
{
m_temporaryConstructionBatches.pop_front();
}
m_temporaryConstructionBatches.emplace_back( std::move( aBatch ) );
}
// Refresh what items are drawn
m_constructionGeomPreview.ClearDrawables();
m_involvedItems.clear();
const auto addBatchItems = [&]( const CONSTRUCTION_ITEM_BATCH& aBatchToAdd, bool aPersistent )
{
for( const CONSTRUCTION_ITEM& item : aBatchToAdd )
{
// Only show the item if it's not already involved
// (avoid double-drawing the same item)
if( m_involvedItems.count( item.Item ) == 0 )
{
m_involvedItems.insert( item.Item );
for( const KIGFX::CONSTRUCTION_GEOM::DRAWABLE& construction : item.Constructions )
{
m_constructionGeomPreview.AddDrawable( construction, aPersistent );
}
}
}
};
if( m_persistentConstructionBatch )
{
addBatchItems( *m_persistentConstructionBatch, true );
}
for( const CONSTRUCTION_ITEM_BATCH& batch : m_temporaryConstructionBatches )
{
addBatchItems( batch, false );
}
updateView();
}
bool CONSTRUCTION_MANAGER::InvolvesAllGivenRealItems( const std::vector<EDA_ITEM*>& aItems ) const
{
for( EDA_ITEM* item : aItems )
{
// Null items (i.e. construction items) are always considered involved
if( item && m_involvedItems.count( item ) == 0 )
{
return false;
}
}
return true;
}
void CONSTRUCTION_MANAGER::SetSnapLineOrigin( const VECTOR2I& aOrigin )
{
// Setting the origin clears the snap line as the end point is no longer valid
ClearSnapLine();
m_snapLineOrigin = aOrigin;
}
void CONSTRUCTION_MANAGER::SetSnapLineEnd( const OPT_VECTOR2I& aSnapEnd )
{
if( m_snapLineOrigin && aSnapEnd != m_snapLineEnd )
{
m_snapLineEnd = aSnapEnd;
if( m_snapLineEnd )
m_constructionGeomPreview.SetSnapLine( SEG{ *m_snapLineOrigin, *m_snapLineEnd } );
else
m_constructionGeomPreview.ClearSnapLine();
updateView();
}
}
void CONSTRUCTION_MANAGER::ClearSnapLine()
{
m_snapLineOrigin.reset();
m_snapLineEnd.reset();
m_constructionGeomPreview.ClearSnapLine();
updateView();
}
void CONSTRUCTION_MANAGER::SetSnappedAnchor( const VECTOR2I& aAnchorPos )
{
if( m_snapLineOrigin )
{
if( aAnchorPos.x == m_snapLineOrigin->x || aAnchorPos.y == m_snapLineOrigin->y )
{
SetSnapLineEnd( aAnchorPos );
}
else
{
// Snapped to something that is not the snap line origin, so
// this anchor is now the new snap line origin
SetSnapLineOrigin( aAnchorPos );
}
}
else
{
// If there's no snap line, start one
m_snapLineOrigin = aAnchorPos;
}
}
std::vector<CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH>
CONSTRUCTION_MANAGER::GetConstructionItems() const
{
std::vector<CONSTRUCTION_ITEM_BATCH> batches;
if( m_persistentConstructionBatch )
{
batches.push_back( *m_persistentConstructionBatch );
}
for( const CONSTRUCTION_ITEM_BATCH& batch : m_temporaryConstructionBatches )
{
batches.push_back( batch );
}
if( m_snapLineOrigin )
{
CONSTRUCTION_ITEM_BATCH batch;
CONSTRUCTION_ITEM& snapPointItem = batch.emplace_back( CONSTRUCTION_ITEM{
SOURCE::FROM_SNAP_LINE,
nullptr,
{},
} );
snapPointItem.Constructions.push_back(
LINE{ *m_snapLineOrigin, *m_snapLineOrigin + VECTOR2I( 100000, 0 ) } );
snapPointItem.Constructions.push_back(
LINE{ *m_snapLineOrigin, *m_snapLineOrigin + VECTOR2I( 0, 100000 ) } );
batches.push_back( std::move( batch ) );
}
return batches;
}
/**
* Check if the cursor has moved far enough away from the snap line origin to escape snapping
* in the X direction.
*
* This is defined as within aEscapeRange of the snap line origin, and within aLongRangeEscapeAngle
* of the vertical line passing through the snap line origin.
*/
static bool pointHasEscapedSnapLineX( const VECTOR2I& aCursor, const VECTOR2I& aSnapLineOrigin,
int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle )
{
if( std::abs( aCursor.x - aSnapLineOrigin.x ) < aEscapeRange )
{
return false;
}
EDA_ANGLE angle = EDA_ANGLE( aCursor - aSnapLineOrigin ) + EDA_ANGLE( 90, DEGREES_T );
return std::abs( angle.Normalize90() ) > aLongRangeEscapeAngle;
}
/**
* As above, but for the Y direction.
*/
static bool pointHasEscapedSnapLineY( const VECTOR2I& aCursor, const VECTOR2I& aSnapLineOrigin,
int aEscapeRange, EDA_ANGLE aLongRangeEscapeAngle )
{
if( std::abs( aCursor.y - aSnapLineOrigin.y ) < aEscapeRange )
{
return false;
}
EDA_ANGLE angle = EDA_ANGLE( aCursor - aSnapLineOrigin );
return std::abs( angle.Normalize90() ) > aLongRangeEscapeAngle;
}
OPT_VECTOR2I CONSTRUCTION_MANAGER::GetNearestSnapLinePoint( const VECTOR2I& aCursor,
const VECTOR2I& aNearestGrid,
std::optional<int> aDistToNearest,
int aSnapRange ) const
{
// return std::nullopt;
if( m_snapLineOrigin )
{
bool snapLine = false;
VECTOR2I bestSnapPoint = aNearestGrid;
// If there's no snap anchor, or it's too far away, prefer the grid
const bool gridBetterThanNearest = !aDistToNearest || *aDistToNearest > aSnapRange;
// The escape range is how far you go before the snap line is de-activated.
// Make this a bit more forgiving than the snap range, as you can easily cancel
// deliberately with a mouse move.
// These are both a bit arbitrary, and can be adjusted as preferred
const int escapeRange = 2 * aSnapRange;
const EDA_ANGLE longRangeEscapeAngle( 3, DEGREES_T );
const bool escapedX = pointHasEscapedSnapLineX( aCursor, *m_snapLineOrigin, escapeRange,
longRangeEscapeAngle );
const bool escapedY = pointHasEscapedSnapLineY( aCursor, *m_snapLineOrigin, escapeRange,
longRangeEscapeAngle );
/// Allows de-snapping from the line if you are closer to another snap point
/// Or if you have moved far enough away from the line
if( !escapedX && gridBetterThanNearest )
{
bestSnapPoint.x = m_snapLineOrigin->x;
snapLine = true;
}
if( !escapedY && gridBetterThanNearest )
{
bestSnapPoint.y = m_snapLineOrigin->y;
snapLine = true;
}
if( snapLine )
{
return bestSnapPoint;
}
}
return std::nullopt;
}

54
common/tool/grid_helper.cpp

@ -21,32 +21,78 @@
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include "tool/grid_helper.h"
#include <functional>
using namespace std::placeholders;
#include <gal/graphics_abstraction_layer.h>
#include <gal/painter.h>
#include <math/util.h> // for KiROUND
#include <math/vector2d.h>
#include <render_settings.h>
#include <tool/tool_manager.h>
#include <view/view.h>
#include <tool/grid_helper.h>
#include <settings/app_settings.h>
GRID_HELPER::GRID_HELPER( TOOL_MANAGER* aToolMgr ) :
m_toolMgr( aToolMgr )
GRID_HELPER::GRID_HELPER( TOOL_MANAGER* aToolMgr, int aConstructionLayer ) :
m_toolMgr( aToolMgr ), m_constructionManager( m_constructionGeomPreview )
{
m_maskTypes = ALL;
m_enableSnap = true;
m_enableSnapLine = true;
m_enableGrid = true;
m_snapItem = std::nullopt;
KIGFX::VIEW* view = m_toolMgr->GetView();
KIGFX::RENDER_SETTINGS* settings = view->GetPainter()->GetSettings();
const KIGFX::COLOR4D constructionColour = settings->GetLayerColor( aConstructionLayer );
m_constructionGeomPreview.SetPersistentColor( constructionColour );
m_constructionGeomPreview.SetColor( constructionColour.WithAlpha( 0.7 ) );
view->Add( &m_constructionGeomPreview );
view->SetVisible( &m_constructionGeomPreview, false );
m_constructionManager.SetUpdateCallback(
[view, this]( bool aAnythingShown )
{
const bool currentlyVisible = view->IsVisible( &m_constructionGeomPreview );
if( currentlyVisible && aAnythingShown )
{
view->Update( &m_constructionGeomPreview, KIGFX::GEOMETRY );
}
else
{
view->SetVisible( &m_constructionGeomPreview, aAnythingShown );
}
} );
}
GRID_HELPER::~GRID_HELPER()
{
KIGFX::VIEW* view = m_toolMgr->GetView();
view->Remove( &m_constructionGeomPreview );
}
void GRID_HELPER::showConstructionGeometry( bool aShow )
{
m_toolMgr->GetView()->SetVisible( &m_constructionGeomPreview, aShow );
}
void GRID_HELPER::updateSnapPoint( const TYPED_POINT2I& aPoint )
{
m_viewSnapPoint.SetPosition( aPoint.m_point );
m_viewSnapPoint.SetSnapTypes( aPoint.m_types );
if( m_toolMgr->GetView()->IsVisible( &m_viewSnapPoint ) )
m_toolMgr->GetView()->Update( &m_viewSnapPoint, KIGFX::GEOMETRY );
else
m_toolMgr->GetView()->SetVisible( &m_viewSnapPoint, true );
}

65
eeschema/tools/ee_grid_helper.cpp

@ -39,7 +39,7 @@
EE_GRID_HELPER::EE_GRID_HELPER( TOOL_MANAGER* aToolMgr ) :
GRID_HELPER( aToolMgr )
GRID_HELPER( aToolMgr, LAYER_SCHEMATIC_ANCHOR )
{
KIGFX::VIEW* view = m_toolMgr->GetView();
@ -55,12 +55,6 @@ EE_GRID_HELPER::EE_GRID_HELPER( TOOL_MANAGER* aToolMgr ) :
m_viewSnapPoint.SetDrawAtZero( true );
view->Add( &m_viewSnapPoint );
view->SetVisible( &m_viewSnapPoint, false );
m_viewSnapLine.SetStyle( KIGFX::ORIGIN_VIEWITEM::DASH_LINE );
m_viewSnapLine.SetColor( COLOR4D( 0.33, 0.55, 0.95, 1.0 ) );
m_viewSnapLine.SetDrawAtZero( true );
view->Add( &m_viewSnapLine );
view->SetVisible( &m_viewSnapLine, false );
}
@ -70,7 +64,6 @@ EE_GRID_HELPER::~EE_GRID_HELPER()
view->Remove( &m_viewAxis );
view->Remove( &m_viewSnapPoint );
view->Remove( &m_viewSnapLine );
}
@ -167,19 +160,24 @@ VECTOR2I EE_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, GRID_HELPER_GR
ANCHOR* nearest = nearestAnchor( aOrigin, SNAPPABLE, aGrid );
VECTOR2I nearestGrid = Align( aOrigin, aGrid );
if( m_enableSnapLine && m_snapItem && m_skipPoint != VECTOR2I( m_viewSnapLine.GetPosition() ) )
showConstructionGeometry( m_enableSnap );
std::optional<VECTOR2I> snapLineOrigin = getConstructionManager().GetSnapLineOrigin();
if( m_enableSnapLine && m_snapItem && snapLineOrigin.has_value()
&& m_skipPoint != *snapLineOrigin )
{
if( std::abs( m_viewSnapLine.GetPosition().x - aOrigin.x ) < snapDist.x )
if( std::abs( snapLineOrigin->x - aOrigin.x ) < snapDist.x )
{
pt.x = m_viewSnapLine.GetPosition().x;
snapDist.x = std::abs( m_viewSnapLine.GetPosition().x - aOrigin.x );
pt.x = snapLineOrigin->x;
snapDist.x = std::abs( pt.x - aOrigin.x );
snapLineX = true;
}
if( std::abs( m_viewSnapLine.GetPosition().y - aOrigin.y ) < snapDist.y )
if( std::abs( snapLineOrigin->y - aOrigin.y ) < snapDist.y )
{
pt.y = m_viewSnapLine.GetPosition().y;
snapDist.y = std::abs( m_viewSnapLine.GetPosition().y - aOrigin.y );
pt.y = snapLineOrigin->y;
snapDist.y = std::abs( pt.y - aOrigin.y );
snapLineY = true;
}
@ -218,7 +216,8 @@ VECTOR2I EE_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, GRID_HELPER_GR
snapPoint = true;
}
snapLineX = snapLineY = false;
snapLineX = false;
snapLineY = false;
gridChecked = true;
}
@ -227,20 +226,14 @@ VECTOR2I EE_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, GRID_HELPER_GR
if( snapLineX || snapLineY )
{
m_viewSnapLine.SetEndPosition( pt );
if( m_toolMgr->GetView()->IsVisible( &m_viewSnapLine ) )
m_toolMgr->GetView()->Update( &m_viewSnapLine, KIGFX::GEOMETRY );
else
m_toolMgr->GetView()->SetVisible( &m_viewSnapLine, true );
getConstructionManager().SetSnapLineEnd( pt );
}
else if( snapPoint )
{
m_snapItem = *nearest;
m_viewSnapPoint.SetPosition( pt );
m_viewSnapLine.SetPosition( pt );
m_toolMgr->GetView()->SetVisible( &m_viewSnapLine, false );
getConstructionManager().SetSnapLineOrigin( pt );
if( m_toolMgr->GetView()->IsVisible( &m_viewSnapPoint ) )
m_toolMgr->GetView()->Update( &m_viewSnapPoint, KIGFX::GEOMETRY);
@ -249,8 +242,8 @@ VECTOR2I EE_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, GRID_HELPER_GR
}
else
{
getConstructionManager().ClearSnapLine();
m_toolMgr->GetView()->SetVisible( &m_viewSnapPoint, false );
m_toolMgr->GetView()->SetVisible( &m_viewSnapLine, false );
}
return pt;
@ -309,7 +302,10 @@ SCH_ITEM* EE_GRID_HELPER::GetSnapped() const
if( !m_snapItem )
return nullptr;
return static_cast<SCH_ITEM*>( m_snapItem->item );
if( m_snapItem->items.empty() )
return nullptr;
return static_cast<SCH_ITEM*>( m_snapItem->items[0] );
}
@ -511,15 +507,20 @@ EE_GRID_HELPER::ANCHOR* EE_GRID_HELPER::nearestAnchor( const VECTOR2I& aPos, int
for( ANCHOR& a : m_anchors )
{
SCH_ITEM* item = static_cast<SCH_ITEM*>( a.item );
if( ( aFlags & a.flags ) != aFlags )
continue;
if( aGrid == GRID_CONNECTABLE && !item->IsConnectable() )
continue;
else if( aGrid == GRID_GRAPHICS && item->IsConnectable() )
continue;
// A "virtual" anchor with no real items associated shouldn't be filtered out
if( !a.items.empty() )
{
// Filter using the first item
SCH_ITEM* item = static_cast<SCH_ITEM*>( a.items[0] );
if( aGrid == GRID_CONNECTABLE && !item->IsConnectable() )
continue;
else if( aGrid == GRID_GRAPHICS && item->IsConnectable() )
continue;
}
double dist = a.Distance( aPos );

84
include/preview_items/construction_geom.h

@ -0,0 +1,84 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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
*/
#pragma once
#include <variant>
#include <vector>
#include <eda_item.h>
#include <gal/color4d.h>
#include <geometry/circle.h>
#include <geometry/half_line.h>
#include <geometry/line.h>
#include <geometry/seg.h>
#include <geometry/shape_arc.h>
namespace KIGFX
{
/**
* Shows construction geometry for things like line extensions, arc centers, etc.
*/
class CONSTRUCTION_GEOM : public EDA_ITEM
{
public:
// Supported items
using DRAWABLE = std::variant<SEG, LINE, HALF_LINE, CIRCLE, SHAPE_ARC, VECTOR2I>;
CONSTRUCTION_GEOM();
wxString GetClass() const override { return wxT( "CONSTRUCTION_GEOM" ); }
const BOX2I ViewBBox() const override;
void ViewDraw( int aLayer, VIEW* aView ) const override;
void ViewGetLayers( int aLayers[], int& aCount ) const override;
void SetColor( const COLOR4D& aColor ) { m_color = aColor; }
void SetPersistentColor( const COLOR4D& aColor ) { m_persistentColor = aColor; }
void AddDrawable( const DRAWABLE& aItem, bool aIsPersistent );
void ClearDrawables();
void SetSnapLine( const SEG& aLine ) { m_snapLine = aLine; }
void ClearSnapLine() { m_snapLine.reset(); }
private:
COLOR4D m_color;
COLOR4D m_persistentColor;
struct DRAWABLE_INFO
{
DRAWABLE Item;
bool IsPersistent;
};
// The items to draw
std::vector<DRAWABLE_INFO> m_drawables;
// The snap line to draw
std::optional<SEG> m_snapLine;
};
} // namespace KIGFX

57
include/preview_items/item_drawing_utils.h

@ -0,0 +1,57 @@
/*
* This program source code file is part of KICAD, a free EDA CAD application.
*
* Copyright (C) 2024 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
*/
#pragma once
#include <gal/graphics_abstraction_layer.h>
#include <math/vector2d.h>
/**
* @file item_drawing_utils.h
*
* Utility functions for drawing compound items (i.e. items that
* need more than one GAL Draw call to render) the might be used be more
* than one VIEW_ITEM Draw function.
*/
namespace KIGFX
{
/**
* Draw a cross at a given position.
*
* @param aGal The graphics abstraction layer to draw with.
* @param aPosition The position to draw the cross at.
* @param aSize The size of the cross.
*/
void DrawCross( GAL& aGal, const VECTOR2I& aPosition, int aSize );
/**
* Draw a dashed line.
*
* @param aGal The graphics abstraction layer to draw with.
* @param aSeg The line to draw.
* @param aDashSize The size of the dashes.
*/
void DrawDashedLine( GAL& aGal, const SEG& aSeg, double aDashSize );
} // namespace KIGFX

179
include/tool/construction_manager.h

@ -0,0 +1,179 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
*
* @author Maciej Suminski <maciej.suminski@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
*/
#pragma once
#include <vector>
#include <deque>
#include <preview_items/construction_geom.h>
class EDA_ITEM;
/**
* A class that mananges "construction" objects and geometry.
*
* Probably only used by GRID_HELPERs, but it's neater to keep it separate,
* as there's quite a bit of state to manage.
*/
class CONSTRUCTION_MANAGER
{
public:
CONSTRUCTION_MANAGER( KIGFX::CONSTRUCTION_GEOM& aHelper );
/**
* Items to be used for the construction of "virtual" anchors, for example, when snapping to
* a point involving an _extension_ of an existing line or arc.
*
* One item can have multiple construction items (e.g. an arc can have a circle and centre point).
*/
enum class SOURCE
{
FROM_ITEMS,
FROM_SNAP_LINE,
};
struct CONSTRUCTION_ITEM
{
SOURCE Source;
EDA_ITEM* Item;
std::vector<KIGFX::CONSTRUCTION_GEOM::DRAWABLE> Constructions;
};
// A single batch of construction items. Once batch contains all the items (and associated
// construction geometry) that should be shown for one point of interest.
// More than one batch may be shown on the screen at the same time.
using CONSTRUCTION_ITEM_BATCH = std::vector<CONSTRUCTION_ITEM>;
/**
* Add a batch of construction items to the helper.
*
* @param aBatch The batch of construction items to add.
* @param aIsPersistent If true, the batch is considered "persistent" and will always be shown
* (and it will replace any previous persistent batch).
* If false, the batch is temporary and may be pushed out by other batches.
*/
void AddConstructionItems( CONSTRUCTION_ITEM_BATCH aBatch, bool aIsPersistent );
/**
* Check if all 'real' (non-null = constructed) the items in the batch are in the list of items
* currently 'involved' in an active construction.
*/
bool InvolvesAllGivenRealItems( const std::vector<EDA_ITEM*>& aItems ) const;
/**
* Set the reference-only points - these are points that are not snapped to, but can still
* be used for connection to the snap line.
*/
void SetReferenceOnlyPoints( std::vector<VECTOR2I> aPoints )
{
m_referenceOnlyPoints = std::move( aPoints );
}
const std::vector<VECTOR2I>& GetReferenceOnlyPoints() const { return m_referenceOnlyPoints; }
/**
* The snap point is a special point that is located at the last point the cursor
* snapped to. If it is set, the construction manager may add extra construction
* geometry to the helper extending from the snap point origin to the cursor,
* which is the 'snap line'.
*/
void SetSnapLineOrigin( const VECTOR2I& aOrigin );
/**
* Set the end point of the snap line.
*
* Passing std::nullopt will unset the end point, but keep the origin.
*/
void SetSnapLineEnd( const OPT_VECTOR2I& aSnapPoint );
/**
* Clear the snap line origin and end points.
*/
void ClearSnapLine();
std::optional<VECTOR2I> GetSnapLineOrigin() const { return m_snapLineOrigin; }
/**
* Inform the construction manager that an anchor snap is wanted.
*
* This will also update the snap line if appropriate.
*/
void SetSnappedAnchor( const VECTOR2I& aAnchorPos );
// Get the list of additional geometry items that should be considered
std::vector<CONSTRUCTION_ITEM_BATCH> GetConstructionItems() const;
/**
* If the snap line is active, return the best snap point that is closest to the cursor
*
* If there's no active snap line, return std::nullopt.
*
* If there's a snap very near, use that otherwise, use the grid point.
* With this point, snap to it on an H/V axis.
*
* Then, if there's a grid point near, snap to it on an H/V axis
*
* @param aCursor The cursor position
* @param aNearestGrid The nearest grid point to the cursor
* @param aDistToNearest The distance to the nearest non-grid snap point, if any
* @param snapRange The snap range
*/
OPT_VECTOR2I GetNearestSnapLinePoint( const VECTOR2I& aCursor, const VECTOR2I& aNearestGrid,
std::optional<int> aDistToNearest, int snapRange ) const;
using GFX_UPDATE_CALLBACK = std::function<void( bool )>;
/**
* Set the callback to call when the construction geometry changes and a view may need updating.
*/
void SetUpdateCallback( GFX_UPDATE_CALLBACK aCallback ) { m_updateCallback = aCallback; }
private:
void updateView();
// An (external) construction helper view item, that this manager adds/removes
// construction objects to/from.
KIGFX::CONSTRUCTION_GEOM& m_constructionGeomPreview;
// Within one "operation", there is one set of construction items that are
// "persistent", and are always shown. Usually the original item and any
// extensions.
std::optional<CONSTRUCTION_ITEM_BATCH> m_persistentConstructionBatch;
// Temporary construction items are added and removed as needed
std::deque<CONSTRUCTION_ITEM_BATCH> m_temporaryConstructionBatches;
// Set of all items for which construction geometry has been added
std::set<EDA_ITEM*> m_involvedItems;
std::vector<VECTOR2I> m_referenceOnlyPoints;
// If a snap point is "active", extra construction geometry is added to the helper
// extending from the snap point to the cursor.
OPT_VECTOR2I m_snapLineOrigin;
OPT_VECTOR2I m_snapLineEnd;
GFX_UPDATE_CALLBACK m_updateCallback;
};

48
include/tool/grid_helper.h

@ -29,6 +29,8 @@
#include <geometry/point_types.h>
#include <math/vector2d.h>
#include <preview_items/snap_indicator.h>
#include <preview_items/construction_geom.h>
#include <tool/construction_manager.h>
#include <tool/tool_manager.h>
#include <tool/selection.h>
#include <origin_viewitem.h>
@ -50,7 +52,7 @@ enum GRID_HELPER_GRIDS : int
class GRID_HELPER
{
public:
GRID_HELPER( TOOL_MANAGER* aToolMgr );
GRID_HELPER( TOOL_MANAGER* aToolMgr, int aConstructionLayer );
virtual ~GRID_HELPER();
VECTOR2I GetGrid() const;
@ -127,7 +129,11 @@ public:
ORIGIN = 8,
VERTICAL = 16,
HORIZONTAL = 32,
ALL = CORNER | OUTLINE | SNAPPABLE | ORIGIN | VERTICAL | HORIZONTAL
// This anchor comes from 'constructed' geometry (e.g. an intersection
// with something else), and not from some intrinsic point of an item
// (e.g. an endpoint)
CONSTRUCTED = 64,
ALL = CORNER | OUTLINE | SNAPPABLE | ORIGIN | VERTICAL | HORIZONTAL | CONSTRUCTED
};
protected:
@ -143,27 +149,40 @@ protected:
* @param aPointTypes The point types that this anchor represents in geometric terms
* @param aItem The item to which the anchor belongs
*/
ANCHOR( const VECTOR2I& aPos, int aFlags, int aPointTypes, EDA_ITEM* aItem ) :
pos( aPos ), flags( aFlags ), pointTypes( aPointTypes ), item( aItem )
ANCHOR( const VECTOR2I& aPos, int aFlags, int aPointTypes, std::vector<EDA_ITEM*> aItems ) :
pos( aPos ), flags( aFlags ), pointTypes( aPointTypes ),
items( std::move( aItems ) )
{
}
VECTOR2I pos;
int flags;
int pointTypes;
EDA_ITEM* item;
// Items that are associated with this anchor (can be more than one, e.g. for an intersection)
std::vector<EDA_ITEM*> items;
double Distance( const VECTOR2I& aP ) const
{
return VECTOR2D( (double) aP.x - pos.x, (double) aP.y - pos.y ).EuclideanNorm();
}
bool InvolvesItem( const EDA_ITEM& aItem ) const
{
return std::find( items.begin(), items.end(), &aItem ) != items.end();
}
};
void addAnchor( const VECTOR2I& aPos, int aFlags, EDA_ITEM* aItem,
int aPointTypes = POINT_TYPE::PT_NONE )
{
addAnchor( aPos, aFlags, std::vector<EDA_ITEM*>{ aItem }, aPointTypes );
}
void addAnchor( const VECTOR2I& aPos, int aFlags, std::vector<EDA_ITEM*> aItems,
int aPointTypes )
{
if( ( aFlags & m_maskTypes ) == aFlags )
m_anchors.emplace_back( ANCHOR( aPos, aFlags, aPointTypes, aItem ) );
m_anchors.emplace_back( ANCHOR( aPos, aFlags, aPointTypes, std::move( aItems ) ) );
}
void clearAnchors()
@ -181,6 +200,12 @@ protected:
const VECTOR2I& aOffset ) const;
protected:
void showConstructionGeometry( bool aShow );
CONSTRUCTION_MANAGER& getConstructionManager() { return m_constructionManager; }
void updateSnapPoint( const TYPED_POINT2I& aPoint );
std::vector<ANCHOR> m_anchors;
TOOL_MANAGER* m_toolMgr;
@ -196,8 +221,17 @@ protected:
VECTOR2I m_skipPoint; // When drawing a line, we avoid snapping to the
// source point
KIGFX::SNAP_INDICATOR m_viewSnapPoint;
KIGFX::ORIGIN_VIEWITEM m_viewSnapLine;
KIGFX::ORIGIN_VIEWITEM m_viewAxis;
private:
// Construction helper - this is what actually shows construction geometry
// (if any) on the canvas.
KIGFX::CONSTRUCTION_GEOM m_constructionGeomPreview;
// Construction manager - this is what manages the construction geometry
// and deals with updating the construction helper as well as keeping
// track of what geometry is "active" for construction purposes.
CONSTRUCTION_MANAGER m_constructionManager;
};
#endif

6
libs/kimath/CMakeLists.txt

@ -19,19 +19,23 @@ set( KIMATH_SRCS
src/geometry/convex_hull.cpp
src/geometry/direction_45.cpp
src/geometry/geometry_utils.cpp
src/geometry/half_line.cpp
src/geometry/intersection.cpp
src/geometry/line.cpp
src/geometry/nearest.cpp
src/geometry/oval.cpp
src/geometry/seg.cpp
src/geometry/shape.cpp
src/geometry/shape_arc.cpp
src/geometry/shape_collisions.cpp
src/geometry/shape_compound.cpp
src/geometry/shape_file_io.cpp
src/geometry/shape_line_chain.cpp
src/geometry/shape_poly_set.cpp
src/geometry/shape_rect.cpp
src/geometry/shape_compound.cpp
src/geometry/shape_segment.cpp
src/geometry/vector_utils.cpp
src/geometry/shape_utils.cpp
src/geometry/vertex_set.cpp

2
libs/kimath/include/geometry/circle.h

@ -38,6 +38,8 @@ public:
CIRCLE( const CIRCLE& aOther );
bool operator==( const CIRCLE& aOther ) const = default;
/**
* Construct this circle such that it is tangent to the given segments and passes through the
* given point, generating the solution which can be used to fillet both segments.

95
libs/kimath/include/geometry/half_line.h

@ -0,0 +1,95 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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
*/
#pragma once
#include <optional>
#include <geometry/seg.h>
#include <math/box2.h>
/*
* A geometric half-line of infinite length, starting at a given point and extending infinitely.
* A.k.a. a ray.
*
* In terms of geometric ops, a SEG would probably do in most cases, as it
* has the same definition, but a separate class is more explicit and also
* allows compile-time reasoning about the meaning of the object through
* the type system.
*/
class HALF_LINE
{
public:
/**
* Construct a ray from a segment - the ray will start at the segment's A point and
* extend infinitely in the direction of the segment, passing through its B point.
*/
HALF_LINE( const SEG& aSeg ) : m_seg( aSeg ) {}
HALF_LINE( const VECTOR2I& aStart, const VECTOR2I& aOtherContainedPoint ) :
m_seg( aStart, aOtherContainedPoint )
{
}
/**
* Get the start point of the ray.
*/
const VECTOR2I& GetStart() const { return m_seg.A; }
/**
* Get one (of the infinite number) of points that the ray passes through.
*/
const VECTOR2I& GetContainedPoint() const { return m_seg.B; }
bool Contains( const VECTOR2I& aPoint ) const;
OPT_VECTOR2I Intersect( const SEG& aSeg ) const;
OPT_VECTOR2I Intersect( const HALF_LINE& aOther ) const;
/**
* Get the nearest point on the ray to the given point.
*
* This will be the start point of the ray for half the 2D plane.
*/
VECTOR2I NearestPoint( const VECTOR2I& aPoint ) const;
/**
* Based on the ray being identically defined. TODO: this is not geoemetrical equality?!
*/
bool operator==( const HALF_LINE& aOther ) const { return m_seg == aOther.m_seg; }
/**
* Gets the (one of the infinite number of) segments that the ray passes through.
*
* The segment's A point is the start of the ray, and the B point is on the ray.
*/
const SEG& GetContainedSeg() const { return m_seg; }
private:
/// Internally, we can represent a just a segment that the ray passes through
SEG m_seg;
};
std::optional<SEG> ClipHalfLineToBox( const HALF_LINE& aRay, const BOX2I& aBox );

17
libs/kimath/include/geometry/intersection.h

@ -27,17 +27,20 @@
#include <vector>
#include <math/vector2d.h>
#include <math/box2.h>
class SEG;
class CIRCLE;
class SHAPE_ARC;
class SHAPE_RECT;
#include <geometry/circle.h>
#include <geometry/half_line.h>
#include <geometry/line.h>
#include <geometry/seg.h>
#include <geometry/shape_arc.h>
#include <geometry/shape_rect.h>
/**
* A variant type that can hold any of the supported geometry types
* for intersection calculations.
*/
using INTERSECTABLE_GEOM = std::variant<SEG, CIRCLE, SHAPE_ARC, SHAPE_RECT>;
using INTERSECTABLE_GEOM = std::variant<LINE, HALF_LINE, SEG, CIRCLE, SHAPE_ARC, BOX2I>;
/**
* A visitor that visits INTERSECTABLE_GEOM variant objects with another
@ -63,9 +66,11 @@ public:
* other geometry.
*/
void operator()( const SEG& aSeg ) const;
void operator()( const LINE& aLine ) const;
void operator()( const HALF_LINE& aLine ) const;
void operator()( const CIRCLE& aCircle ) const;
void operator()( const SHAPE_ARC& aArc ) const;
void operator()( const SHAPE_RECT& aArc ) const;
void operator()( const BOX2I& aArc ) const;
private:
const INTERSECTABLE_GEOM& m_otherGeometry;

67
libs/kimath/include/geometry/line.h

@ -0,0 +1,67 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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
*/
#pragma once
#include <geometry/seg.h>
/*
* A geometric line of infinite length.
*
* In terms of geometric ops, a SEG would probably do as it has the same definition,
* but a separate class is more explicit and also allows compile-time
* reasoning about the meaning of the object through the type system.
*/
class LINE
{
public:
using ecoord = VECTOR2I::extended_type;
LINE( const SEG& aSeg ) : m_seg( aSeg ) {}
LINE( const VECTOR2I& aStart, const VECTOR2I& aEnd ) : m_seg( aStart, aEnd ) {}
bool operator==( const LINE& aOther ) const { return m_seg == aOther.m_seg; }
/**
* Gets the (one of the infinite number of) segments that the line passes through.
*/
const SEG& GetContainedSeg() const { return m_seg; }
OPT_VECTOR2I Intersect( const SEG& aOther ) const;
OPT_VECTOR2I Intersect( const LINE& aOther ) const;
/**
* Gets the distance from the line to the given point.
*/
int Distance( const VECTOR2I& aPoint ) const;
/**
* Gets the nearest point on the line to the given point.
*/
VECTOR2I NearestPoint( const VECTOR2I& aPoint ) const;
private:
/// Internally, we can represent a just a segment that the line passes through
SEG m_seg;
};

59
libs/kimath/include/geometry/nearest.h

@ -0,0 +1,59 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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
*/
#pragma once
#include <variant>
#include <vector>
#include <math/vector2d.h>
#include <math/box2.h>
#include <geometry/circle.h>
#include <geometry/half_line.h>
#include <geometry/line.h>
#include <geometry/seg.h>
#include <geometry/shape_arc.h>
/**
* A variant type that can hold any of the supported geometry types for
* nearest point calculations.
*/
using NEARABLE_GEOM = std::variant<LINE, HALF_LINE, SEG, CIRCLE, SHAPE_ARC, BOX2I, VECTOR2I>;
/**
* Get the nearest point on a geometry to a given point.
*/
VECTOR2I GetNearestPoint( const NEARABLE_GEOM& aGeom, const VECTOR2I& aPt );
/**
* Get the nearest point on any of a list of geometries to a given point.
*
* @param aGeoms The geometries to check.
* @param aPt The point to find the nearest point to.
*
* @return The nearest point on any of the geometries to the given point (or std::nullopt if
* no geometries were provided).
*/
OPT_VECTOR2I GetNearestPoint( const std::vector<NEARABLE_GEOM>& aGeoms, const VECTOR2I& aPt );

12
libs/kimath/include/geometry/point_types.h

@ -65,15 +65,21 @@ enum POINT_TYPE
* The point is an intersection of two (or more) items.
*/
PT_INTERSECTION = 1 << 5,
/**
* The point is somewhere on another element, but not some specific point.
* (you can infer this from some other point types)
*/
PT_ON_ELEMENT = 1 << 6,
};
struct TYPED_POINT2I
{
VECTOR2I m_point;
POINT_TYPE m_types;
VECTOR2I m_point;
// Bitwise OR of POINT_TYPE values
int m_types;
// Clang needs this apparently
TYPED_POINT2I( const VECTOR2I& aVec, POINT_TYPE aTypes ) : m_point( aVec ), m_types( aTypes ) {}
TYPED_POINT2I( const VECTOR2I& aVec, int aTypes ) : m_point( aVec ), m_types( aTypes ) {}
bool operator==( const TYPED_POINT2I& ) const = default;
};

72
libs/kimath/include/geometry/shape_utils.h

@ -0,0 +1,72 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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
*/
#pragma once
/**
* @file geometry/shape_utils.h
*
* @brief Utility functions for working with shapes.
*
* These are free functions to avoid bloating the shape classes with functions
* that only need to be used in a few places and can just use the public
* interfaces.
*/
#include <array>
#include <optional>
#include <math/vector2d.h>
#include <math/box2.h>
class HALF_LINE;
class LINE;
class SEG;
namespace KIGEOM
{
/**
* Returns a SEG such that the start point is smaller or equal
* in x and y compared to the end point.
*/
SEG NormalisedSeg( const SEG& aSeg );
/**
* Decompose a BOX2 into four segments.
*
* Segments are returned in the order: Top, Right, Bottom, Left.
*/
std::array<SEG, 4> BoxToSegs( const BOX2I& aBox );
/**
* Get the segment of a half-line that is inside a box, if any.
*/
std::optional<SEG> ClipHalfLineToBox( const HALF_LINE& aRay, const BOX2I& aBox );
/**
* Get the segment of a line that is inside a box, if any.
*/
std::optional<SEG> ClipLineToBox( const LINE& aLine, const BOX2I& aBox );
} // namespace KIGEOM

3
libs/kimath/include/geometry/vector_utils.h

@ -28,7 +28,7 @@ namespace KIGEOM
* @file vector_utils.h
*
* Supplemental functions for working with vectors and
* objects that interact with vectors.
* simple objects that interact with vectors.
*/
/*
@ -82,6 +82,7 @@ bool PointProjectsOntoSegment( const VECTOR2I& aPoint, const SEG& aSeg );
/**
* Get the ratio of the vector to a point from the segment's start,
* compared to the segment's length.
*
* /--- aPoint
* A<---+-------->B <-- Length L
* | |

5
libs/kimath/include/math/box2.h

@ -67,6 +67,11 @@ public:
Normalize();
}
static BOX2<Vec> ByCorners( const Vec& aCorner1, const Vec& aCorner2 )
{
return BOX2( aCorner1, aCorner2 - aCorner1 );
}
void SetMaximum()
{
if constexpr( std::is_floating_point<coord_type>() )

13
libs/kimath/include/math/vector2d.h

@ -214,6 +214,10 @@ public:
*/
double Distance( const VECTOR2<extended_type>& aVector ) const;
/**
* Compute the squared distance between two vectors.
*/
constexpr extended_type SquaredDistance( const VECTOR2<T>& aVector ) const;
// Operators
@ -556,6 +560,15 @@ double VECTOR2<T>::Distance( const VECTOR2<extended_type>& aVector ) const
return diff.EuclideanNorm();
}
template <class T>
constexpr typename VECTOR2<T>::extended_type
VECTOR2<T>::SquaredDistance( const VECTOR2<T>& aVector ) const
{
const extended_type dx = (extended_type) x - aVector.x;
const extended_type dy = (extended_type) y - aVector.y;
return dx * dx + dy * dy;
}
template <class T>
bool VECTOR2<T>::operator<( const VECTOR2<T>& aVector ) const

131
libs/kimath/src/geometry/half_line.cpp

@ -0,0 +1,131 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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/half_line.h"
#include <math/box2.h>
#include <geometry/shape_utils.h>
/**
* Check if two vectors point into the same quadrant.
*/
bool VectorsInSameQuadrant( const VECTOR2I& aA, const VECTOR2I& aB )
{
// The sign of the x and y components of the vectors must be the same
return ( ( aA.x >= 0 ) == ( aB.x >= 0 ) ) && ( ( aA.y >= 0 ) == ( aB.y >= 0 ) );
}
bool HALF_LINE::Contains( const VECTOR2I& aPoint ) const
{
// Check that the point is on the right side of the ray from
// the start point
// This is quick, so we can do it first
if( !VectorsInSameQuadrant( m_seg.B - m_seg.A, aPoint - m_seg.A ) )
{
return false;
}
// Check that the point is within a distance of 1 from the
// infinite line of the ray
return m_seg.LineDistance( aPoint ) <= 1;
}
OPT_VECTOR2I HALF_LINE::Intersect( const SEG& aSeg ) const
{
// Intsersection of two infinite lines
const SEG seg = GetContainedSeg();
OPT_VECTOR2I intersection = aSeg.Intersect( seg, false, true );
// Reject parallel lines
if( !intersection )
{
return std::nullopt;
}
// Check that the intersection is on the right side of the
// ray's start point (i.e. equal quadrants)
if( !VectorsInSameQuadrant( m_seg.B - m_seg.A, *intersection - m_seg.A ) )
{
return std::nullopt;
}
// Check that the intersection is not somewhere past the end
// of the segment
if( !aSeg.Contains( *intersection ) )
{
return std::nullopt;
}
return intersection;
}
OPT_VECTOR2I HALF_LINE::Intersect( const HALF_LINE& aOther ) const
{
// Intsersection of two infinite lines
const SEG otherSeg = aOther.GetContainedSeg();
OPT_VECTOR2I intersection = m_seg.Intersect( otherSeg, false, true );
// Reject parallel lines
if( !intersection )
{
return std::nullopt;
}
// Check that the intersection is on the right side of both
// rays' start points (i.e. equal quadrants)
if( !VectorsInSameQuadrant( m_seg.B - m_seg.A, *intersection - m_seg.A )
|| !VectorsInSameQuadrant( aOther.m_seg.B - aOther.m_seg.A,
*intersection - aOther.m_seg.A ) )
{
return std::nullopt;
}
return intersection;
}
VECTOR2I HALF_LINE::NearestPoint( const VECTOR2I& aPoint ) const
{
// Same as the SEG implementation, but without the early return
// if the point isn't on the segment.
// Inlined for performance reasons
VECTOR2L d( m_seg.B.x - m_seg.A.x, m_seg.B.y - m_seg.A.y );
SEG::ecoord l_squared( d.x * d.x + d.y * d.y );
if( l_squared == 0 )
return m_seg.A;
SEG::ecoord t = d.Dot( aPoint - m_seg.A );
if( t < 0 )
return m_seg.A;
SEG::ecoord xp = rescale( t, (SEG::ecoord) d.x, l_squared );
SEG::ecoord yp = rescale( t, (SEG::ecoord) d.y, l_squared );
return VECTOR2<SEG::ecoord>( m_seg.A.x + xp, m_seg.A.y + yp );
}

221
libs/kimath/src/geometry/intersection.cpp

@ -25,10 +25,7 @@
#include <core/type_helpers.h>
#include <geometry/seg.h>
#include <geometry/circle.h>
#include <geometry/shape_arc.h>
#include <geometry/shape_rect.h>
#include <geometry/shape_utils.h>
/*
* Helper functions that dispatch to the correct intersection function
@ -47,6 +44,27 @@ void findIntersections( const SEG& aSegA, const SEG& aSegB, std::vector<VECTOR2I
}
}
void findIntersections( const SEG& aSeg, const LINE& aLine, std::vector<VECTOR2I>& aIntersections )
{
OPT_VECTOR2I intersection = aLine.Intersect( aSeg );
if( intersection )
{
aIntersections.push_back( *intersection );
}
}
void findIntersections( const SEG& aSeg, const HALF_LINE& aHalfLine,
std::vector<VECTOR2I>& aIntersections )
{
OPT_VECTOR2I intersection = aHalfLine.Intersect( aSeg );
if( intersection )
{
aIntersections.push_back( *intersection );
}
}
void findIntersections( const SEG& aSeg, const CIRCLE& aCircle,
std::vector<VECTOR2I>& aIntersections )
{
@ -71,6 +89,68 @@ void findIntersections( const SEG& aSeg, const SHAPE_ARC& aArc,
}
}
void findIntersections( const LINE& aLineA, const LINE& aLineB,
std::vector<VECTOR2I>& aIntersections )
{
OPT_VECTOR2I intersection = aLineA.Intersect( aLineB );
if( intersection )
{
aIntersections.push_back( *intersection );
}
}
void findIntersections( const LINE& aLine, const HALF_LINE& aHalfLine,
std::vector<VECTOR2I>& aIntersections )
{
// Intersect as two infinite lines
OPT_VECTOR2I intersection =
aHalfLine.GetContainedSeg().Intersect( aLine.GetContainedSeg(), false, true );
// No intersection at all (parallel, or passes on the other side of the start point)
if( !intersection )
{
return;
}
if( aHalfLine.Contains( *intersection ) )
{
aIntersections.push_back( *intersection );
}
}
void findIntersections( const HALF_LINE& aHalfLineA, const HALF_LINE& aHalfLineB,
std::vector<VECTOR2I>& aIntersections )
{
OPT_VECTOR2I intersection = aHalfLineA.Intersect( aHalfLineB );
if( intersection )
{
aIntersections.push_back( *intersection );
}
}
void findIntersections( const CIRCLE& aCircle, const LINE& aLine,
std::vector<VECTOR2I>& aIntersections )
{
std::vector<VECTOR2I> intersections = aCircle.IntersectLine( aLine.GetContainedSeg() );
aIntersections.insert( aIntersections.end(), intersections.begin(), intersections.end() );
}
void findIntersections( const CIRCLE& aCircle, const HALF_LINE& aHalfLine,
std::vector<VECTOR2I>& aIntersections )
{
std::vector<VECTOR2I> intersections = aCircle.IntersectLine( aHalfLine.GetContainedSeg() );
for( const VECTOR2I& intersection : intersections )
{
if( aHalfLine.Contains( intersection ) )
{
aIntersections.push_back( intersection );
}
}
}
void findIntersections( const CIRCLE& aCircleA, const CIRCLE& aCircleB,
std::vector<VECTOR2I>& aIntersections )
@ -91,19 +171,28 @@ void findIntersections( const SHAPE_ARC& aArcA, const SHAPE_ARC& aArcB,
aArcA.Intersect( aArcB, &aIntersections );
}
void findIntersections( const SHAPE_ARC& aArc, const LINE& aLine,
std::vector<VECTOR2I>& aIntersections )
{
std::vector<VECTOR2I> intersections;
aArc.IntersectLine( aLine.GetContainedSeg(), &intersections );
aIntersections.insert( aIntersections.end(), intersections.begin(), intersections.end() );
}
std::vector<SEG> RectToSegs( const SHAPE_RECT& aRect )
void findIntersections( const SHAPE_ARC& aArc, const HALF_LINE& aHalfLine,
std::vector<VECTOR2I>& aIntersections )
{
const VECTOR2I corner = aRect.GetPosition();
const int w = aRect.GetWidth();
const int h = aRect.GetHeight();
std::vector<VECTOR2I> intersections;
aArc.IntersectLine( aHalfLine.GetContainedSeg(), &intersections );
return {
SEG( corner, { corner + VECTOR2I( w, 0 ) } ),
SEG( { corner + VECTOR2I( w, 0 ) }, { corner + VECTOR2I( w, h ) } ),
SEG( { corner + VECTOR2I( w, h ) }, { corner + VECTOR2I( 0, h ) } ),
SEG( { corner + VECTOR2I( 0, h ) }, corner ),
};
for( const VECTOR2I& intersection : intersections )
{
if( aHalfLine.Contains( intersection ) )
{
aIntersections.push_back( intersection );
}
}
}
} // namespace
@ -132,10 +221,10 @@ void INTERSECTION_VISITOR::operator()( const SEG& aSeg ) const
{
using OtherGeomType = std::decay_t<decltype( otherGeom )>;
if constexpr( std::is_same_v<OtherGeomType, SHAPE_RECT> )
if constexpr( std::is_same_v<OtherGeomType, BOX2I> )
{
// Seg-Rect via decomposition into segments
for( const SEG& aRectSeg : RectToSegs( otherGeom ) )
for( const SEG& aRectSeg : KIGEOM::BoxToSegs( otherGeom ) )
{
findIntersections( aSeg, aRectSeg, m_intersections );
}
@ -149,6 +238,82 @@ void INTERSECTION_VISITOR::operator()( const SEG& aSeg ) const
m_otherGeometry );
}
void INTERSECTION_VISITOR::operator()( const LINE& aLine ) const
{
// Dispatch to the correct function
return std::visit(
[&]( const auto& otherGeom )
{
using OtherGeomType = std::decay_t<decltype( otherGeom )>;
// Dispatch in the correct order
if constexpr( std::is_same_v<OtherGeomType, SEG>
|| std::is_same_v<OtherGeomType, LINE>
|| std::is_same_v<OtherGeomType, CIRCLE>
|| std::is_same_v<OtherGeomType, SHAPE_ARC> )
{
// Seg-Line, Line-Line, Circle-Line, Arc-Line
findIntersections( otherGeom, aLine, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, HALF_LINE> )
{
// Line-HalfLine
findIntersections( aLine, otherGeom, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, BOX2I> )
{
// Line-Rect via decomposition into segments
for( const SEG& aRectSeg : KIGEOM::BoxToSegs( otherGeom ) )
{
findIntersections( aRectSeg, aLine, m_intersections );
}
}
else
{
static_assert( always_false<OtherGeomType>::value,
"Unhandled other geometry type" );
}
},
m_otherGeometry );
};
void INTERSECTION_VISITOR::operator()( const HALF_LINE& aHalfLine ) const
{
// Dispatch to the correct function
return std::visit(
[&]( const auto& otherGeom )
{
using OtherGeomType = std::decay_t<decltype( otherGeom )>;
// Dispatch in the correct order
if constexpr( std::is_same_v<OtherGeomType, SEG>
|| std::is_same_v<OtherGeomType, HALF_LINE>
|| std::is_same_v<OtherGeomType, CIRCLE>
|| std::is_same_v<OtherGeomType, SHAPE_ARC> )
{
// Seg-HalfLine, HalfLine-HalfLine, Circle-HalfLine, Arc-HalfLine
findIntersections( otherGeom, aHalfLine, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, LINE> )
{
// Line-HalfLine
findIntersections( otherGeom, aHalfLine, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, BOX2I> )
{
// HalfLine-Rect via decomposition into segments
for( const SEG& aRectSeg : KIGEOM::BoxToSegs( otherGeom ) )
{
findIntersections( aRectSeg, aHalfLine, m_intersections );
}
}
else
{
static_assert( always_false<OtherGeomType>::value,
"Unhandled other geometry type" );
}
},
m_otherGeometry );
};
void INTERSECTION_VISITOR::operator()( const CIRCLE& aCircle ) const
{
// Dispatch to the correct function
@ -163,15 +328,17 @@ void INTERSECTION_VISITOR::operator()( const CIRCLE& aCircle ) const
// Seg-Circle, Circle-Circle
findIntersections( otherGeom, aCircle, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, SHAPE_ARC> )
else if constexpr( std::is_same_v<OtherGeomType, SHAPE_ARC>
|| std::is_same_v<OtherGeomType, LINE>
|| std::is_same_v<OtherGeomType, HALF_LINE> )
{
// Circle-Arc
// Circle-Arc, Circle-Line, Circle-HalfLine
findIntersections( aCircle, otherGeom, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, SHAPE_RECT> )
else if constexpr( std::is_same_v<OtherGeomType, BOX2I> )
{
// Circle-Rect via decomposition into segments
for( const SEG& aRectSeg : RectToSegs( otherGeom ) )
for( const SEG& aRectSeg : KIGEOM::BoxToSegs( otherGeom ) )
{
findIntersections( aRectSeg, aCircle, m_intersections );
}
@ -200,10 +367,16 @@ void INTERSECTION_VISITOR::operator()( const SHAPE_ARC& aArc ) const
// Seg-Arc, Circle-Arc, Arc-Arc
findIntersections( otherGeom, aArc, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, SHAPE_RECT> )
else if constexpr( std::is_same_v<OtherGeomType, LINE>
|| std::is_same_v<OtherGeomType, HALF_LINE> )
{
// Arc-Line, Arc-HalfLine
findIntersections( aArc, otherGeom, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, BOX2I> )
{
// Arc-Rect via decomposition into segments
for( const SEG& aRectSeg : RectToSegs( otherGeom ) )
for( const SEG& aRectSeg : KIGEOM::BoxToSegs( otherGeom ) )
{
findIntersections( aRectSeg, aArc, m_intersections );
}
@ -218,13 +391,13 @@ void INTERSECTION_VISITOR::operator()( const SHAPE_ARC& aArc ) const
};
void INTERSECTION_VISITOR::operator()( const SHAPE_RECT& aRect ) const
void INTERSECTION_VISITOR::operator()( const BOX2I& aRect ) const
{
// Defer to the SEG visitor repeatedly
// Note - in some cases, points can be repeated in the intersection list
// if that's an issue, both directions of the visitor can be implemented
// to take care of that.
const std::vector<SEG> segs = RectToSegs( aRect );
const std::array<SEG, 4> segs = KIGEOM::BoxToSegs( aRect );
for( const SEG& seg : segs )
{

70
libs/kimath/src/geometry/line.cpp

@ -0,0 +1,70 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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 <http://www.gnu.org/licenses/>.
*/
#include <geometry/line.h>
OPT_VECTOR2I LINE::Intersect( const SEG& aSeg ) const
{
// intersect as two lines
OPT_VECTOR2I intersection = aSeg.Intersect( m_seg, false, true );
if( intersection )
{
// Not parallel.
// That was two lines, but we need to check if the intersection is on
// the requested segment
if( aSeg.Contains( *intersection ) )
{
return intersection;
}
}
return std::nullopt;
}
OPT_VECTOR2I LINE::Intersect( const LINE& aOther ) const
{
// Defer to the SEG implementation
return aOther.m_seg.Intersect( m_seg, false, true );
}
int LINE::Distance( const VECTOR2I& aPoint ) const
{
// Just defer to the SEG implementation
return m_seg.LineDistance( aPoint );
}
VECTOR2I LINE::NearestPoint( const VECTOR2I& aPoint ) const
{
// Same as the SEG implementation, but without the early return
// if the point isn't on the segment.
// Inlined for performance reasons
VECTOR2L d( m_seg.B.x - m_seg.A.x, m_seg.B.y - m_seg.A.y );
ecoord l_squared( d.x * d.x + d.y * d.y );
if( l_squared == 0 )
return m_seg.A;
ecoord t = d.Dot( aPoint - m_seg.A );
ecoord xp = rescale( t, (ecoord) d.x, l_squared );
ecoord yp = rescale( t, (ecoord) d.y, l_squared );
return VECTOR2<ecoord>( m_seg.A.x + xp, m_seg.A.y + yp );
}

114
libs/kimath/src/geometry/nearest.cpp

@ -0,0 +1,114 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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/nearest.h"
#include <wx/debug.h>
#include <core/type_helpers.h>
#include <geometry/shape_utils.h>
namespace
{
VECTOR2I NearestPoint( const BOX2I& aBox, const VECTOR2I& aPt )
{
VECTOR2I nearest;
int bestDistance = std::numeric_limits<int>::max();
for( const SEG& seg : KIGEOM::BoxToSegs( aBox ) )
{
const VECTOR2I nearestSegPt = seg.NearestPoint( aPt );
const int thisDistance = nearestSegPt.Distance( aPt );
if( thisDistance <= bestDistance )
{
nearest = nearestSegPt;
bestDistance = thisDistance;
}
}
return nearest;
};
} // namespace
VECTOR2I GetNearestPoint( const NEARABLE_GEOM& aGeom, const VECTOR2I& aPt )
{
VECTOR2I nearest;
std::visit(
[&]( const auto& geom )
{
using GeomType = std::decay_t<decltype( geom )>;
if constexpr( std::is_same_v<GeomType, LINE>
|| std::is_same_v<GeomType, HALF_LINE>
|| std::is_same_v<GeomType, SEG>
|| std::is_same_v<GeomType, CIRCLE>
|| std::is_same_v<GeomType, SHAPE_ARC> )
{
// Same signatures for all these types
// But they're not in the same polymorphic hierarchy
nearest = geom.NearestPoint( aPt );
}
else if constexpr( std::is_same_v<GeomType, BOX2I> )
{
// Defer to the utils function
nearest = NearestPoint( geom, aPt );
}
else if constexpr( std::is_same_v<GeomType, VECTOR2I> )
{
nearest = geom;
}
else
{
static_assert( always_false<GeomType>::value, "non-exhaustive visitor" );
}
},
aGeom );
return nearest;
}
OPT_VECTOR2I GetNearestPoint( const std::vector<NEARABLE_GEOM>& aGeoms, const VECTOR2I& aPt )
{
OPT_VECTOR2I nearestPointOnAny;
int bestDistance = std::numeric_limits<int>::max();
for( const NEARABLE_GEOM& geom : aGeoms )
{
const VECTOR2I thisNearest = GetNearestPoint( geom, aPt );
const int thisDistance = thisNearest.Distance( aPt );
if( !nearestPointOnAny || thisDistance < bestDistance )
{
nearestPointOnAny = thisNearest;
bestDistance = thisDistance;
}
}
return nearestPointOnAny;
}

141
libs/kimath/src/geometry/shape_utils.cpp

@ -0,0 +1,141 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2024 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/shape_utils.h"
#include <geometry/seg.h>
#include <geometry/half_line.h>
#include <geometry/line.h>
SEG KIGEOM::NormalisedSeg( const SEG& aSeg )
{
if( LexicographicalCompare( aSeg.A, aSeg.B ) <= 0 )
{
return aSeg;
}
return aSeg.Reversed();
}
std::array<SEG, 4> KIGEOM::BoxToSegs( const BOX2I& aBox )
{
const std::array<VECTOR2I, 4> corners = {
VECTOR2I{ aBox.GetLeft(), aBox.GetTop() },
VECTOR2I{ aBox.GetRight(), aBox.GetTop() },
VECTOR2I{ aBox.GetRight(), aBox.GetBottom() },
VECTOR2I{ aBox.GetLeft(), aBox.GetBottom() },
};
return {
SEG{ corners[0], corners[1] },
SEG{ corners[1], corners[2] },
SEG{ corners[2], corners[3] },
SEG{ corners[3], corners[0] },
};
}
std::optional<SEG> KIGEOM::ClipHalfLineToBox( const HALF_LINE& aRay, const BOX2I& aBox )
{
// Do the naive implementation - if this really is done in a tight loop,
// the Cohen-Sutherland implementation in ClipLine could be faster, but
// needs to be adapted to work with half-lines.
const std::array<SEG, 4> boxSegs = KIGEOM::BoxToSegs( aBox );
std::optional<VECTOR2I> ptA, ptB;
for( const SEG& boxSeg : boxSegs )
{
OPT_VECTOR2I intersection = aRay.Intersect( boxSeg );
if( !intersection )
{
continue;
}
// Init the first point or eat it if it's the same
if( !ptA || *intersection == *ptA )
{
ptA = *intersection;
}
else
{
ptB = *intersection;
}
}
// If we have exactly two intersections, the ray crossed twice
// so take the segment between the two points
if( ptA && ptB )
{
return SEG( *ptA, *ptB );
}
// It only crosses once, so the start is in the box. Take the segment from
// the start point to the intersection
if( ptA && *ptA != aRay.GetStart() )
{
return SEG( aRay.GetStart(), *ptA );
}
// It didn't cross at all
return std::nullopt;
}
std::optional<SEG> KIGEOM::ClipLineToBox( const LINE& aLine, const BOX2I& aBox )
{
// As above, maybe can be optimised?
const std::array<SEG, 4> boxSegs = KIGEOM::BoxToSegs( aBox );
std::optional<VECTOR2I> ptA, ptB;
for( const SEG& boxSeg : boxSegs )
{
OPT_VECTOR2I intersection = aLine.Intersect( boxSeg );
// Reject intersections that are not on the actual box boundary
if( intersection && boxSeg.Contains( *intersection ) )
{
// Init the first point or eat it if it's the same
if( !ptA || *intersection == *ptA )
{
ptA = *intersection;
}
else
{
ptB = *intersection;
}
}
}
// If we have exactly two intersections, we have a segment
// (zero is no intersection, and one is a just crossing a corner exactly)
if( ptA && ptB )
{
return SEG( *ptA, *ptB );
}
return std::nullopt;
}

677
pcbnew/tools/pcb_grid_helper.cpp

@ -36,12 +36,14 @@
#include <zone.h>
#include <gal/graphics_abstraction_layer.h>
#include <geometry/intersection.h>
#include <geometry/nearest.h>
#include <geometry/oval.h>
#include <geometry/shape_circle.h>
#include <geometry/shape_line_chain.h>
#include <geometry/shape_rect.h>
#include <geometry/shape_segment.h>
#include <geometry/shape_simple.h>
#include <geometry/shape_utils.h>
#include <macros.h>
#include <math/util.h> // for KiROUND
#include <gal/painter.h>
@ -50,14 +52,100 @@
#include <tools/pcb_tool_base.h>
#include <view/view.h>
namespace
{
/**
* Get the INTERSECTABLE_GEOM for a BOARD_ITEM if it's supported.
*
* This is the idealised geometry, e.g. a zero-width line or circle.
*/
std::optional<INTERSECTABLE_GEOM> GetBoardIntersectable( const BOARD_ITEM& aItem )
{
switch( aItem.Type() )
{
case PCB_SHAPE_T:
{
const PCB_SHAPE& shape = static_cast<const PCB_SHAPE&>( aItem );
switch( shape.GetShape() )
{
case SHAPE_T::SEGMENT: return SEG{ shape.GetStart(), shape.GetEnd() };
case SHAPE_T::CIRCLE: return CIRCLE{ shape.GetCenter(), shape.GetRadius() };
case SHAPE_T::ARC:
return SHAPE_ARC{ shape.GetStart(), shape.GetArcMid(), shape.GetEnd(), 0 };
case SHAPE_T::RECTANGLE: return BOX2I::ByCorners( shape.GetStart(), shape.GetEnd() );
default: break;
}
break;
}
case PCB_TRACE_T:
{
const PCB_TRACK& track = static_cast<const PCB_TRACK&>( aItem );
return SEG{ track.GetStart(), track.GetEnd() };
}
case PCB_ARC_T:
{
const PCB_ARC& arc = static_cast<const PCB_ARC&>( aItem );
return SHAPE_ARC{ arc.GetStart(), arc.GetMid(), arc.GetEnd(), 0 };
}
default: break;
}
return std::nullopt;
}
/**
* Find the closest point on a BOARD_ITEM to a given point.
*
* Only works for items that have a NEARABLE_GEOM defined, it's
* not a general purpose function.
*
* @return The closest point on the item to aPos, or std::nullopt if the item
* doesn't have a NEARABLE_GEOM defined.
*/
std::optional<int64_t> FindSquareDistanceToItem( const BOARD_ITEM& item, const VECTOR2I& aPos )
{
std::optional<INTERSECTABLE_GEOM> intersectable = GetBoardIntersectable( item );
std::optional<NEARABLE_GEOM> nearable;
if( intersectable )
{
// Exploit the intersectable as a nearable
std::visit(
[&]( auto& geom )
{
nearable = NEARABLE_GEOM( std::move( geom ) );
},
*intersectable );
}
// Whatever the item is, we don't have a nearable for it
if( !nearable )
return std::nullopt;
const VECTOR2I nearestPt = GetNearestPoint( *nearable, aPos );
return nearestPt.SquaredDistance( aPos );
}
} // namespace
PCB_GRID_HELPER::PCB_GRID_HELPER( TOOL_MANAGER* aToolMgr, MAGNETIC_SETTINGS* aMagneticSettings ) :
GRID_HELPER( aToolMgr ),
m_magneticSettings( aMagneticSettings )
GRID_HELPER( aToolMgr, LAYER_ANCHOR ), m_magneticSettings( aMagneticSettings )
{
KIGFX::VIEW* view = m_toolMgr->GetView();
KIGFX::RENDER_SETTINGS* settings = view->GetPainter()->GetSettings();
KIGFX::COLOR4D auxItemsColor = settings->GetLayerColor( LAYER_AUX_ITEMS );
KIGFX::COLOR4D umbilicalColor = settings->GetLayerColor( LAYER_ANCHOR );
m_viewAxis.SetSize( 20000 );
m_viewAxis.SetStyle( KIGFX::ORIGIN_VIEWITEM::CROSS );
@ -72,12 +160,6 @@ PCB_GRID_HELPER::PCB_GRID_HELPER( TOOL_MANAGER* aToolMgr, MAGNETIC_SETTINGS* aMa
m_viewSnapPoint.SetDrawAtZero( true );
view->Add( &m_viewSnapPoint );
view->SetVisible( &m_viewSnapPoint, false );
m_viewSnapLine.SetStyle( KIGFX::ORIGIN_VIEWITEM::DASH_LINE );
m_viewSnapLine.SetColor( umbilicalColor );
m_viewSnapLine.SetDrawAtZero( true );
view->Add( &m_viewSnapLine );
view->SetVisible( &m_viewSnapLine, false );
}
@ -87,7 +169,114 @@ PCB_GRID_HELPER::~PCB_GRID_HELPER()
view->Remove( &m_viewAxis );
view->Remove( &m_viewSnapPoint );
view->Remove( &m_viewSnapLine );
}
void PCB_GRID_HELPER::AddConstructionItems( std::vector<BOARD_ITEM*> aItems, bool aExtensionOnly,
bool aIsPersistent )
{
// For all the elements that get drawn construction geometry,
// add something suitable to the construction helper.
// This can be nothing.
CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH constructionItemsBatch;
std::vector<VECTOR2I> referenceOnlyPoints;
for( BOARD_ITEM* item : aItems )
{
std::vector<KIGFX::CONSTRUCTION_GEOM::DRAWABLE> constructionDrawables;
if( item->Type() == PCB_SHAPE_T )
{
PCB_SHAPE& shape = static_cast<PCB_SHAPE&>( *item );
switch( shape.GetShape() )
{
case SHAPE_T::SEGMENT:
{
if( !aExtensionOnly )
{
constructionDrawables.emplace_back( LINE{ shape.GetStart(), shape.GetEnd() } );
}
else
{
// Two rays, extending from the segment ends
const VECTOR2I segVec = shape.GetEnd() - shape.GetStart();
constructionDrawables.emplace_back(
HALF_LINE{ shape.GetStart(), shape.GetStart() - segVec } );
constructionDrawables.emplace_back(
HALF_LINE{ shape.GetEnd(), shape.GetEnd() + segVec } );
}
if( aIsPersistent )
{
// include the original endpoints as construction items
// (this allows H/V snapping)
constructionDrawables.emplace_back( shape.GetStart() );
constructionDrawables.emplace_back( shape.GetEnd() );
// But mark them as references, so they don't get snapped to themsevles
referenceOnlyPoints.emplace_back( shape.GetStart() );
referenceOnlyPoints.emplace_back( shape.GetEnd() );
}
break;
}
case SHAPE_T::ARC:
{
if( !aExtensionOnly )
{
constructionDrawables.push_back(
CIRCLE{ shape.GetCenter(), shape.GetRadius() } );
}
else
{
// The rest of the circle is the arc through the opposite point to the midpoint
const VECTOR2I oppositeMid =
shape.GetCenter() + ( shape.GetCenter() - shape.GetArcMid() );
constructionDrawables.push_back(
SHAPE_ARC{ shape.GetStart(), oppositeMid, shape.GetEnd(), 0 } );
}
constructionDrawables.push_back( shape.GetCenter() );
if( aIsPersistent )
{
// include the original endpoints as construction items
// (this allows H/V snapping)
constructionDrawables.emplace_back( shape.GetStart() );
constructionDrawables.emplace_back( shape.GetEnd() );
// But mark them as references, so they don't get snapped to themselves
referenceOnlyPoints.emplace_back( shape.GetStart() );
referenceOnlyPoints.emplace_back( shape.GetEnd() );
}
break;
}
case SHAPE_T::CIRCLE:
case SHAPE_T::RECTANGLE:
{
constructionDrawables.push_back( shape.GetCenter() );
break;
}
default:
// This shape doesn't have any construction geometry to draw
break;
}
}
constructionItemsBatch.emplace_back( CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM{
CONSTRUCTION_MANAGER::SOURCE::FROM_ITEMS, item,
std::move( constructionDrawables ) } );
}
if( referenceOnlyPoints.size() )
{
getConstructionManager().SetReferenceOnlyPoints( std::move( referenceOnlyPoints ) );
}
// Let the manager handle it
getConstructionManager().AddConstructionItems( std::move( constructionItemsBatch ),
aIsPersistent );
}
@ -203,12 +392,16 @@ VECTOR2I PCB_GRID_HELPER::AlignToNearestPad( const VECTOR2I& aMousePos, std::deq
for( ANCHOR& a : m_anchors )
{
BOARD_ITEM* item = static_cast<BOARD_ITEM*>( a.item );
if( ( ORIGIN & a.flags ) != ORIGIN )
continue;
if( !item->HitTest( aMousePos ) )
bool hitAny = true;
for( EDA_ITEM* item : m_snapItem->items )
{
hitAny = hitAny && item->HitTest( aMousePos );
}
if( !hitAny )
continue;
double dist = a.Distance( aMousePos );
@ -231,14 +424,13 @@ VECTOR2I PCB_GRID_HELPER::BestDragOrigin( const VECTOR2I &aMousePos,
{
clearAnchors();
computeAnchors( aItems, aMousePos, true, aSelectionFilter );
computeAnchors( aItems, aMousePos, true, aSelectionFilter, nullptr, true );
double worldScale = m_toolMgr->GetView()->GetGAL()->GetWorldScale();
double lineSnapMinCornerDistance = 50.0 / worldScale;
double lineSnapMinCornerDistance = m_toolMgr->GetView()->ToWorld( 50 );
ANCHOR* nearestOutline = nearestAnchor( aMousePos, OUTLINE, LSET::AllLayersMask() );
ANCHOR* nearestCorner = nearestAnchor( aMousePos, CORNER, LSET::AllLayersMask() );
ANCHOR* nearestOrigin = nearestAnchor( aMousePos, ORIGIN, LSET::AllLayersMask() );
ANCHOR* nearestOutline = nearestAnchor( aMousePos, OUTLINE );
ANCHOR* nearestCorner = nearestAnchor( aMousePos, CORNER );
ANCHOR* nearestOrigin = nearestAnchor( aMousePos, ORIGIN );
ANCHOR* best = nullptr;
double minDist = std::numeric_limits<double>::max();
@ -290,6 +482,7 @@ VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, BOARD_ITEM* a
return BestSnapAnchor( aOrigin, layers, aGrid, item );
}
VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& aLayers,
GRID_HELPER_GRIDS aGrid,
const std::vector<BOARD_ITEM*>& aSkip )
@ -302,84 +495,172 @@ VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& a
// see https://gitlab.com/kicad/code/kicad/-/issues/5638
// see https://gitlab.com/kicad/code/kicad/-/issues/7125
// see https://gitlab.com/kicad/code/kicad/-/issues/12303
double snapScale = snapSize / m_toolMgr->GetView()->GetGAL()->GetWorldScale();
double snapScale = m_toolMgr->GetView()->ToWorld( snapSize );
// warning: GetVisibleGrid().x sometimes returns a value > INT_MAX. Intermediate calculation
// needs double.
int snapRange = KiROUND( m_enableGrid ? std::min( snapScale, GetVisibleGrid().x ) : snapScale );
int snapDist = snapRange;
//Respect limits of coordinates representation
BOX2I bb;
bb.SetOrigin( GetClampedCoords<double, int>( VECTOR2D( aOrigin ) - snapRange / 2.0 ) );
bb.SetEnd( GetClampedCoords<double, int>( VECTOR2D( aOrigin ) + snapRange / 2.0 ) );
const BOX2I visibilityHorizon =
BOX2ISafe( VECTOR2D( aOrigin ) - snapRange / 2.0, VECTOR2D( snapRange, snapRange ) );
clearAnchors();
const std::vector<BOARD_ITEM*> visibleItems = queryVisible( bb, aSkip );
computeAnchors( visibleItems, aOrigin, false, nullptr );
const std::vector<BOARD_ITEM*> visibleItems = queryVisible( visibilityHorizon, aSkip );
computeAnchors( visibleItems, aOrigin, false, nullptr, &aLayers, false );
ANCHOR* nearest = nearestAnchor( aOrigin, SNAPPABLE, aLayers );
ANCHOR* nearest = nearestAnchor( aOrigin, SNAPPABLE );
VECTOR2I nearestGrid = Align( aOrigin, aGrid );
// The distance to the nearest snap point, if any
std::optional<int> snapDist;
if( nearest )
snapDist = nearest->Distance( aOrigin );
// Existing snap lines need priority over new snaps
if( m_snapItem && m_enableSnapLine && m_enableSnap )
showConstructionGeometry( m_enableSnap );
CONSTRUCTION_MANAGER& constructionManager = getConstructionManager();
const auto ptIsReferenceOnly = [&]( const VECTOR2I& aPt )
{
bool snapLine = false;
int x_dist = std::abs( m_viewSnapLine.GetPosition().x - aOrigin.x );
int y_dist = std::abs( m_viewSnapLine.GetPosition().y - aOrigin.y );
const std::vector<VECTOR2I>& referenceOnlyPoints =
constructionManager.GetReferenceOnlyPoints();
return std::find( referenceOnlyPoints.begin(), referenceOnlyPoints.end(), aPt )
!= referenceOnlyPoints.end();
};
/// Allows de-snapping from the line if you are closer to another snap point
if( x_dist < snapRange && ( !nearest || snapDist > snapRange ) )
if( m_enableSnap )
{
// Existing snap lines need priority over new snaps
if( m_enableSnapLine )
{
nearestGrid.x = m_viewSnapLine.GetPosition().x;
snapLine = true;
}
OPT_VECTOR2I snapLineSnap = constructionManager.GetNearestSnapLinePoint(
aOrigin, nearestGrid, snapDist, snapRange );
if( y_dist < snapRange && ( !nearest || snapDist > snapRange ) )
{
nearestGrid.y = m_viewSnapLine.GetPosition().y;
snapLine = true;
// We found a better snap point that the nearest one
if( snapLineSnap && m_skipPoint != *snapLineSnap )
{
constructionManager.SetSnapLineEnd( *snapLineSnap );
// Don't show a snap point if we're snapping to a grid rather than an anchor
m_toolMgr->GetView()->SetVisible( &m_viewSnapPoint, false );
m_viewSnapPoint.SetSnapTypes( POINT_TYPE::PT_NONE );
// Only return the snap line end as a snap if it's not a reference point
// (we don't snap to reference points, but we can use them to update the snap line,
// without actually snapping)
if( !ptIsReferenceOnly( *snapLineSnap ) )
{
return *snapLineSnap;
}
}
}
if( snapLine && m_skipPoint != VECTOR2I( m_viewSnapLine.GetPosition() ) )
// If there's a snap anchor within range, use it
if( nearest && nearest->Distance( aOrigin ) <= snapRange )
{
m_viewSnapLine.SetEndPosition( nearestGrid );
const bool anchorIsConstructed = nearest->flags & ANCHOR_FLAGS::CONSTRUCTED;
if( m_toolMgr->GetView()->IsVisible( &m_viewSnapLine ) )
m_toolMgr->GetView()->Update( &m_viewSnapLine, KIGFX::GEOMETRY );
// If the nearest anchor is a reference point, we don't snap to it,
// but we can update the snap line origin
if( ptIsReferenceOnly( nearest->pos ) )
{
// We can set the snap line origin, but don't mess with the
// accepted snap point
constructionManager.SetSnapLineOrigin( nearest->pos );
}
else
m_toolMgr->GetView()->SetVisible( &m_viewSnapLine, true );
{
// Only 'intrinsic' points of items can trigger adding more construction items
// (so just mousing over the intersection of an item doesn't add a construction item
// for the second item). This is to make construction items less intrusive and more
// a result of user intent.
if( !anchorIsConstructed )
{
// Add any involved item as a temporary construction item
// (de-duplication with existing construction items is handled later)
std::vector<BOARD_ITEM*> items;
return nearestGrid;
for( EDA_ITEM* item : nearest->items )
{
BOARD_ITEM* boardItem = static_cast<BOARD_ITEM*>( item );
// Null items are allowed to arrive here as they represent geometry that isn't
// specifically tied to a board item. For example snap lines from some
// other anchor.
// But they don't produce new construction items.
if( boardItem )
{
if( m_magneticSettings->allLayers
|| ( ( aLayers & boardItem->GetLayerSet() ).any() ) )
{
items.push_back( boardItem );
}
}
}
// Temporary construction items are not persistent and don't
// overlay the items themselves (as the items will not be moved)
AddConstructionItems( items, true, false );
}
const auto shouldAcceptAnchor = [&]( const ANCHOR& aAnchor )
{
// Check that any involved real items are 'active'
// (i.e. the user has moused over a key point previously)
// If any are not real (e.g. snap lines), they are allowed to be involved
//
// This is an area most likely to be controversial/need tuning,
// as some users will think it's fiddly; without 'activation', others will
// think the snaps are intrusive.
bool allRealAreInvolved =
constructionManager.InvolvesAllGivenRealItems( aAnchor.items );
return allRealAreInvolved;
};
if( shouldAcceptAnchor( *nearest ) )
{
m_snapItem = *nearest;
// Set the snap line origin or end as needed
constructionManager.SetSnappedAnchor( m_snapItem->pos );
// Show the right snap point marker
updateSnapPoint( { m_snapItem->pos, m_snapItem->pointTypes } );
return m_snapItem->pos;
}
}
}
}
if( nearest && m_enableSnap )
{
if( nearest->Distance( aOrigin ) <= snapRange )
{
m_viewSnapPoint.SetPosition( nearest->pos );
m_viewSnapLine.SetPosition( nearest->pos );
m_toolMgr->GetView()->SetVisible( &m_viewSnapLine, false );
// If we got here, we didn't snap to an anchor or snap line
m_viewSnapPoint.SetSnapTypes( nearest->pointTypes );
// If we're snapping to a grid, on-element snaps would be too intrusive
// but they're useful when there isn't a grid to snap to
if( !m_enableGrid )
{
OPT_VECTOR2I nearestPointOnAnElement =
GetNearestPoint( m_pointOnLineCandidates, aOrigin );
if( m_toolMgr->GetView()->IsVisible( &m_viewSnapPoint ) )
m_toolMgr->GetView()->Update( &m_viewSnapPoint, KIGFX::GEOMETRY);
else
m_toolMgr->GetView()->SetVisible( &m_viewSnapPoint, true );
// Got any nearest point - snap if in range
if( nearestPointOnAnElement
&& nearestPointOnAnElement->Distance( aOrigin ) <= snapRange )
{
updateSnapPoint( { *nearestPointOnAnElement, POINT_TYPE::PT_ON_ELEMENT } );
m_snapItem = *nearest;
return nearest->pos;
// Clear the snap end, but keep the origin so touching another line
// doesn't kill a snap line
constructionManager.SetSnapLineEnd( std::nullopt );
return *nearestPointOnAnElement;
}
}
}
// Completely failed to find any snap point, so snap to the grid
m_snapItem = std::nullopt;
constructionManager.SetSnapLineEnd( std::nullopt );
m_toolMgr->GetView()->SetVisible( &m_viewSnapPoint, false );
m_toolMgr->GetView()->SetVisible( &m_viewSnapLine, false );
return nearestGrid;
}
@ -389,7 +670,12 @@ BOARD_ITEM* PCB_GRID_HELPER::GetSnapped() const
if( !m_snapItem )
return nullptr;
return static_cast<BOARD_ITEM*>( m_snapItem->item );
// The snap anchor doesn't have an item associated with it
// (odd, could it be entirely made of construction geometry?)
if( m_snapItem->items.empty() )
return nullptr;
return static_cast<BOARD_ITEM*>( m_snapItem->items[0] );
}
@ -557,81 +843,97 @@ struct PCB_INTERSECTABLE
void PCB_GRID_HELPER::computeAnchors( const std::vector<BOARD_ITEM*>& aItems,
const VECTOR2I& aRefPos, bool aFrom,
const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter )
const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter,
const LSET* aMatchLayers, bool aForDrag )
{
std::vector<PCB_INTERSECTABLE> intersectables;
// This c/should come from a more granular snap filter
const bool computeIntersections = true;
// These could come from a more granular snap mode filter
// But when looking for drag points, we don't want construction geometry
const bool computeIntersections = !aForDrag;
const bool computePointsOnElements = !aForDrag;
const bool excludeGraphics = aSelectionFilter && !aSelectionFilter->graphics;
const bool excludeTracks = aSelectionFilter && !aSelectionFilter->tracks;
for( BOARD_ITEM* item : aItems )
const auto itemIsSnappable = [&]( const BOARD_ITEM& aItem )
{
// If we are filtering by layers, check if the item matches
if( aMatchLayers )
{
return m_magneticSettings->allLayers
|| ( ( *aMatchLayers & aItem.GetLayerSet() ).any() );
}
return true;
};
const auto processItem = [&]( BOARD_ITEM& item )
{
// Don't even process the item if it doesn't match the layers
if( !itemIsSnappable( item ) )
{
return;
}
// First, add all the key points of the item itself
computeAnchors( item, aRefPos, aFrom, aSelectionFilter );
computeAnchors( &item, aRefPos, aFrom, aSelectionFilter );
// If we are computing intersections, construct the relevant intersectables
if( computeIntersections )
// Points on elements also use the intersectables.
if( computeIntersections || computePointsOnElements )
{
if( !excludeGraphics && item->Type() == PCB_SHAPE_T )
std::optional<INTERSECTABLE_GEOM> intersectableGeom;
if( !excludeGraphics && item.Type() == PCB_SHAPE_T )
{
PCB_SHAPE& shape = static_cast<PCB_SHAPE&>( *item );
switch( shape.GetShape() )
{
case SHAPE_T::SEGMENT:
{
intersectables.emplace_back( &shape, SEG{ shape.GetStart(), shape.GetEnd() } );
break;
}
case SHAPE_T::CIRCLE:
{
intersectables.emplace_back( &shape,
CIRCLE{ shape.GetCenter(), shape.GetRadius() } );
break;
}
case SHAPE_T::ARC:
{
intersectables.emplace_back(
&shape,
SHAPE_ARC{ shape.GetStart(), shape.GetArcMid(), shape.GetEnd(), 0 } );
break;
}
case SHAPE_T::RECTANGLE:
{
intersectables.emplace_back( &shape,
SHAPE_RECT{ shape.GetStart(), shape.GetEnd() } );
break;
}
default:
// Ignore other shapes
break;
}
intersectableGeom = GetBoardIntersectable( item );
}
else if( !excludeTracks )
else if( !excludeTracks && ( item.Type() == PCB_TRACE_T || item.Type() == PCB_ARC_T ) )
{
switch( item->Type() )
{
case PCB_TRACE_T:
{
PCB_TRACK& track = static_cast<PCB_TRACK&>( *item );
intersectableGeom = GetBoardIntersectable( item );
}
intersectables.emplace_back( &track, SEG{ track.GetStart(), track.GetEnd() } );
break;
}
case PCB_ARC_T:
{
PCB_ARC& arc = static_cast<PCB_ARC&>( *item );
if( intersectableGeom )
{
intersectables.emplace_back( &item, *intersectableGeom );
}
}
};
intersectables.emplace_back(
&arc, SHAPE_ARC{ arc.GetStart(), arc.GetMid(), arc.GetEnd(), 0 } );
break;
}
default:
// Ignore other items
break;
}
for( BOARD_ITEM* item : aItems )
{
processItem( *item );
}
for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM_BATCH& batch :
getConstructionManager().GetConstructionItems() )
{
for( const CONSTRUCTION_MANAGER::CONSTRUCTION_ITEM& constructionItem : batch )
{
BOARD_ITEM* involvedItem = static_cast<BOARD_ITEM*>( constructionItem.Item );
for( const KIGFX::CONSTRUCTION_GEOM::DRAWABLE& drawable :
constructionItem.Constructions )
{
std::visit(
[&]( const auto& visited )
{
using ItemType = std::decay_t<decltype( visited )>;
if constexpr( std::is_same_v<ItemType, LINE>
|| std::is_same_v<ItemType, CIRCLE>
|| std::is_same_v<ItemType, HALF_LINE>
|| std::is_same_v<ItemType, SHAPE_ARC> )
{
intersectables.emplace_back( involvedItem, visited );
}
else if constexpr( std::is_same_v<ItemType, VECTOR2I> )
{
// Add any free-floating points as snap points.
addAnchor( visited, SNAPPABLE | CONSTRUCTED, involvedItem,
POINT_TYPE::PT_NONE );
}
},
drawable );
}
}
}
@ -639,24 +941,58 @@ void PCB_GRID_HELPER::computeAnchors( const std::vector<BOARD_ITEM*>& aItems,
// Now, add all the intersections between the items
// This is obviously quadratic, so performance may be a concern for large selections
// But, so far up to ~20k comparisons seems not to be an issue with run times in the ms range
// and it's usually only a handful of items.
for( size_t ii = 0; ii < intersectables.size(); ++ii )
if( computeIntersections )
{
std::vector<VECTOR2I> intersections;
const PCB_INTERSECTABLE& intersectableA = intersectables[ii];
for( size_t ii = 0; ii < intersectables.size(); ++ii )
{
const PCB_INTERSECTABLE& intersectableA = intersectables[ii];
const INTERSECTION_VISITOR visitor{ intersectableA.Geometry, intersections };
for( size_t jj = ii + 1; jj < intersectables.size(); ++jj )
{
const PCB_INTERSECTABLE& intersectableB = intersectables[jj];
for( size_t jj = ii + 1; jj < intersectables.size(); ++jj )
{
const PCB_INTERSECTABLE& intersectableB = intersectables[jj];
std::visit( visitor, intersectableB.Geometry );
// An item and its own extension will often have intersections (as they are on top of each other),
// but they not useful points to snap to
if( intersectableA.Item == intersectableB.Item )
continue;
std::vector<VECTOR2I> intersections;
const INTERSECTION_VISITOR visitor{ intersectableA.Geometry, intersections };
std::visit( visitor, intersectableB.Geometry );
// For each intersection, add an intersection snap anchor
for( const VECTOR2I& intersection : intersections )
{
std::vector<EDA_ITEM*> items = {
intersectableA.Item,
intersectableB.Item,
};
addAnchor( intersection, SNAPPABLE | CONSTRUCTED, std::move( items ),
POINT_TYPE::PT_INTERSECTION );
}
}
}
}
// For each intersection, add an intersection snap anchor
for( const VECTOR2I& intersection : intersections )
// The intersectables can also be used for fall-back snapping to "point on line"
// snaps if no other snap is found
m_pointOnLineCandidates.clear();
if( computePointsOnElements )
{
// For the moment, it's trivial to make a NEARABLE from an INTERSECTABLE,
// because all INTERSECTABLEs are also NEARABLEs.
for( const PCB_INTERSECTABLE& intersectable : intersectables )
{
addAnchor( intersection, SNAPPABLE, intersectableA.Item, POINT_TYPE::PT_INTERSECTION );
std::visit(
[&]( const auto& geom )
{
NEARABLE_GEOM nearable( geom );
m_pointOnLineCandidates.emplace_back( nearable );
},
intersectable.Geometry );
}
}
}
@ -851,7 +1187,7 @@ void PCB_GRID_HELPER::computeAnchors( BOARD_ITEM* aItem, const VECTOR2I& aRefPos
addAnchor( shape->GetStart(), CORNER | SNAPPABLE, shape,
POINT_TYPE::PT_END );
addAnchor( shape->GetEnd(), CORNER | SNAPPABLE, shape,
POINT_TYPE::PT_CORNER );
POINT_TYPE::PT_END );
addAnchor( shape->GetArcMid(), CORNER | SNAPPABLE, shape,
POINT_TYPE::PT_MID );
addAnchor( shape->GetCenter(), ORIGIN | SNAPPABLE, shape,
@ -1176,30 +1512,73 @@ void PCB_GRID_HELPER::computeAnchors( BOARD_ITEM* aItem, const VECTOR2I& aRefPos
}
PCB_GRID_HELPER::ANCHOR* PCB_GRID_HELPER::nearestAnchor( const VECTOR2I& aPos, int aFlags,
LSET aMatchLayers )
PCB_GRID_HELPER::ANCHOR* PCB_GRID_HELPER::nearestAnchor( const VECTOR2I& aPos, int aFlags )
{
double minDist = std::numeric_limits<double>::max();
// Do this all in squared distances as we only care about relative distances
using ecoord = VECTOR2I::extended_type;
ecoord minDist = std::numeric_limits<ecoord>::max();
std::vector<ANCHOR*> anchorsAtMinDistance;
for( ANCHOR& anchor : m_anchors )
{
// There is no need to filter by layers here, as the items are already filtered
// by layer (if needed) when the anchors are computed.
if( ( aFlags & anchor.flags ) != aFlags )
continue;
if( !anchorsAtMinDistance.empty() && anchor.pos == anchorsAtMinDistance.front()->pos )
{
// Same distance as the previous best anchor
anchorsAtMinDistance.push_back( &anchor );
}
else
{
const double dist = anchor.pos.SquaredDistance( aPos );
if( dist < minDist )
{
// New minimum distance
minDist = dist;
anchorsAtMinDistance.clear();
anchorsAtMinDistance.push_back( &anchor );
}
}
}
// More than one anchor can be at the same distance, for example
// two lines end-to-end each have the same endpoint anchor.
// So, check which one has an involved item that's closest to the origin,
// and use that one (which allows the user to choose which items
// gets extended - it's the one nearest the cursor)
ecoord minDistToItem = std::numeric_limits<ecoord>::max();
ANCHOR* best = nullptr;
for( ANCHOR& a : m_anchors )
// One of the anchors at the minimum distance
for( ANCHOR* const anchor : anchorsAtMinDistance )
{
BOARD_ITEM* item = static_cast<BOARD_ITEM*>( a.item );
ecoord distToNearestItem = std::numeric_limits<ecoord>::max();
for( EDA_ITEM* const item : anchor->items )
{
if( !item )
continue;
if( !m_magneticSettings->allLayers && ( ( aMatchLayers & item->GetLayerSet() ).none() ) )
continue;
std::optional<ecoord> distToThisItem =
FindSquareDistanceToItem( static_cast<const BOARD_ITEM&>( *item ), aPos );
if( ( aFlags & a.flags ) != aFlags )
continue;
if( distToThisItem )
distToNearestItem = std::min( distToNearestItem, *distToThisItem );
}
double dist = a.Distance( aPos );
// If the item doesn't have any special min-dist handler,
// just use the distance to the anchor
distToNearestItem = std::min( distToNearestItem, minDist );
if( dist < minDist )
if( distToNearestItem < minDistToItem )
{
minDist = dist;
best = &a;
minDistToItem = distToNearestItem;
best = anchor;
}
}
return best;
}
}

40
pcbnew/tools/pcb_grid_helper.h

@ -31,6 +31,9 @@
#include <pcb_item_containers.h>
#include <tool/grid_helper.h>
#include <board.h>
#include <geometry/intersection.h>
#include <geometry/nearest.h>
class LSET;
class SHAPE_ARC;
@ -69,9 +72,17 @@ public:
virtual void OnBoardItemRemoved( BOARD& aBoard, BOARD_ITEM* aBoardItem ) override
{
if( m_snapItem->item == aBoardItem )
// If the item being removed is involved in the snap, clear the snap item
if( m_snapItem )
{
m_snapItem = std::nullopt;
for( EDA_ITEM* item : m_snapItem->items )
{
if( item == aBoardItem )
{
m_snapItem = std::nullopt;
break;
}
}
}
}
@ -93,13 +104,28 @@ public:
VECTOR2D GetGridSize( GRID_HELPER_GRIDS aGrid ) const override;
/**
* Add construction geometry for a set of board items.
*
* @param aItems The items for which to add construction geometry
* @param aExtensionOnly If true, the construction geometry only includes extensions of the
* items, if false it also overlays the items themselves.
* @param aIsPersistent If true, the construction geometry is considered "persistent" and will
* always be shown and won't be replaced by later temporary geometry.
*/
void AddConstructionItems( std::vector<BOARD_ITEM*> aItems, bool aExtensionOnly,
bool aIsPersistent );
private:
std::vector<BOARD_ITEM*> queryVisible( const BOX2I& aArea,
const std::vector<BOARD_ITEM*>& aSkip ) const;
ANCHOR* nearestAnchor( const VECTOR2I& aPos, int aFlags, LSET aMatchLayers );
/**
* Find the nearest anchor point to the given position with matching flags.
*
* @param return The nearest anchor point, or nullptr if none found
*/
ANCHOR* nearestAnchor( const VECTOR2I& aPos, int aFlags );
/**
* computeAnchors inserts the local anchor points in to the grid helper for the specified
@ -107,7 +133,8 @@ private:
* between the items.
*/
void computeAnchors( const std::vector<BOARD_ITEM*>& aItems, const VECTOR2I& aRefPos,
bool aFrom, const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter );
bool aFrom, const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter,
const LSET* aLayers, bool aForDrag );
/**
* computeAnchors inserts the local anchor points in to the grid helper for the specified
@ -120,8 +147,9 @@ private:
void computeAnchors( BOARD_ITEM* aItem, const VECTOR2I& aRefPos, bool aFrom,
const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter );
private:
MAGNETIC_SETTINGS* m_magneticSettings;
std::vector<NEARABLE_GEOM> m_pointOnLineCandidates;
};
#endif

11
pcbnew/tools/pcb_point_editor.cpp

@ -516,6 +516,10 @@ int PCB_POINT_EDITOR::OnSelectionChange( const TOOL_EVENT& aEvent )
getViewControls()->ShowCursor( true );
PCB_GRID_HELPER grid( m_toolMgr, editFrame->GetMagneticItemsSettings() );
// Use the original object as a construction item
std::unique_ptr<BOARD_ITEM> clone;
m_editPoints = makePoints( item );
if( !m_editPoints )
@ -604,6 +608,13 @@ int PCB_POINT_EDITOR::OnSelectionChange( const TOOL_EVENT& aEvent )
if( &point != m_editedPoint )
point.SetActive( false );
}
// When we start dragging, create a clone of the item to use as the original
// reference geometry (e.g. for intersections and extensions)
std::unique_ptr<BOARD_ITEM> oldClone = std::move( clone );
clone.reset( static_cast<BOARD_ITEM*>( item->Clone() ) );
grid.AddConstructionItems( { clone.get() }, false, true );
// Now old clone can be safely deleted
}
// Keep point inside of limits with some padding

3
qa/tests/libs/kimath/CMakeLists.txt

@ -29,12 +29,13 @@ set( QA_KIMATH_SRCS
geometry/geom_test_utils.cpp
geometry/test_chamfer.cpp
geometry/test_circle.cpp
geometry/test_distribute.cpp
geometry/test_dogbone.cpp
geometry/test_eda_angle.cpp
geometry/test_ellipse_to_bezier.cpp
geometry/test_fillet.cpp
geometry/test_circle.cpp
geometry/test_half_line.cpp
geometry/test_oval.cpp
geometry/test_segment.cpp
geometry/test_shape_compound_collision.cpp

8
qa/tests/libs/kimath/geometry/geom_test_utils.cpp

@ -31,14 +31,8 @@ std::string toString( const POINT_TYPE& aType )
}
}
std::ostream& operator<<( std::ostream& os, const POINT_TYPE& aPtType )
{
os << "POINT_TYPE: " << toString( aPtType );
return os;
}
std::ostream& operator<<( std::ostream& os, const TYPED_POINT2I& aPt )
{
os << "TYPED_POINT2I: " << aPt.m_point << " (" << toString( aPt.m_types ) << ")";
os << "TYPED_POINT2I: " << aPt.m_point << " (" << aPt.m_types << ")";
return os;
}

1
qa/tests/libs/kimath/geometry/geom_test_utils.h

@ -334,6 +334,5 @@ std::ostream& boost_test_print_type( std::ostream& os, const SHAPE_LINE_CHAIN& c
// Not clear why boost_test_print_type doesn't work on Debian specifically for this type,
// but this works on all platforms
std::ostream& operator<<( std::ostream& os, const TYPED_POINT2I& c );
std::ostream& operator<<( std::ostream& os, const POINT_TYPE& c );
#endif // GEOM_TEST_UTILS_H

193
qa/tests/libs/kimath/geometry/test_half_line.cpp

@ -0,0 +1,193 @@
/*
* 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 <qa_utils/wx_utils/unit_test_utils.h>
#include <geometry/half_line.h>
#include <geometry/shape_utils.h>
#include "geom_test_utils.h"
struct HalfLineFixture
{
};
struct HalfLineBoxClipCase
{
std::string Description;
HALF_LINE Hl;
BOX2I Box;
std::optional<SEG> ExpectedClippedSeg;
};
struct HalfLineHalfLineIntersectionCase
{
std::string Description;
HALF_LINE HlA;
HALF_LINE HlB;
std::optional<VECTOR2I> ExpectedIntersection;
};
struct HalfLineContainsPointCase
{
std::string Description;
HALF_LINE Hl;
VECTOR2I Point;
bool ExpectedContains;
};
BOOST_FIXTURE_TEST_SUITE( HalfLine, HalfLineFixture )
BOOST_AUTO_TEST_CASE( Contains )
{
const std::vector<HalfLineContainsPointCase> cases{
{
"Point on the ray",
HALF_LINE( SEG( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ) ),
VECTOR2I( 50, 50 ),
true,
},
{
"Point on the ray start",
HALF_LINE( SEG( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ) ),
VECTOR2I( 0, 0 ),
true,
},
{
"Point on the ray end",
HALF_LINE( SEG( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ) ),
VECTOR2I( 100, 100 ),
true,
},
{
"Point on the ray, past the end",
HALF_LINE( SEG( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ) ),
VECTOR2I( 150, 150 ),
true,
},
{
"Point on the infinite line, but on the wrong side",
HALF_LINE( SEG( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ) ),
VECTOR2I( -50, -50 ),
false,
},
{
"Point to one side",
HALF_LINE( SEG( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ) ),
VECTOR2I( 50, 0 ),
false,
}
};
for( const auto& c : cases )
{
BOOST_TEST_INFO( c.Description );
const bool contains = c.Hl.Contains( c.Point );
BOOST_TEST( contains == c.ExpectedContains );
}
}
BOOST_AUTO_TEST_CASE( Intersect )
{
const std::vector<HalfLineHalfLineIntersectionCase> cases{
{
"Simple cross",
HALF_LINE( SEG( VECTOR2I( -100, -100 ), VECTOR2I( 0, 0 ) ) ),
HALF_LINE( SEG( VECTOR2I( 100, -100 ), VECTOR2I( 0, 0 ) ) ),
VECTOR2I( 0, 0 ),
},
{
"Parallel, no intersection",
HALF_LINE( SEG( VECTOR2I( -100, 0 ), VECTOR2I( -100, 100 ) ) ),
HALF_LINE( SEG( VECTOR2I( 100, 0 ), VECTOR2I( 100, 100 ) ) ),
std::nullopt,
}
};
for( const auto& c : cases )
{
BOOST_TEST_INFO( c.Description );
std::optional<VECTOR2I> intersection = c.HlA.Intersect( c.HlB );
BOOST_REQUIRE( intersection.has_value() == c.ExpectedIntersection.has_value() );
if( !intersection )
continue;
BOOST_TEST( *intersection == *c.ExpectedIntersection );
}
}
BOOST_AUTO_TEST_CASE( ClipToBox )
{
const std::vector<HalfLineBoxClipCase> cases{
{
"Center to right edge",
HALF_LINE( SEG( VECTOR2I( 0, 0 ), VECTOR2I( 100, 0 ) ) ),
BOX2I{ VECTOR2{ -1000, -1000 }, VECTOR2{ 2000, 2000 } },
SEG( VECTOR2I( 0, 0 ), VECTOR2I( 1000, 0 ) ),
},
{
"Centre to corner",
HALF_LINE( SEG( VECTOR2I( 0, 0 ), VECTOR2I( 100, 100 ) ) ),
BOX2I{ VECTOR2{ -1000, -1000 }, VECTOR2{ 2000, 2000 } },
SEG( VECTOR2I( 0, 0 ), VECTOR2I( 1000, 1000 ) ),
},
{
"Ray not in the box",
HALF_LINE( SEG( VECTOR2I( 1500, 0 ), VECTOR2I( 1600, 0 ) ) ),
BOX2I{ VECTOR2{ -1000, -1000 }, VECTOR2{ 2000, 2000 } },
std::nullopt,
},
{
"Ray starts outside but crosses box",
HALF_LINE( SEG( VECTOR2I( -1500, 0 ), VECTOR2I( 0, 0 ) ) ),
BOX2I{ VECTOR2{ -1000, -1000 }, VECTOR2{ 2000, 2000 } },
SEG( VECTOR2I( -1000, 0 ), VECTOR2I( 1000, 0 ) ),
},
};
for( const auto& c : cases )
{
BOOST_TEST_INFO( c.Description );
std::optional<SEG> clipped = KIGEOM::ClipHalfLineToBox( c.Hl, c.Box );
BOOST_REQUIRE( clipped.has_value() == c.ExpectedClippedSeg.has_value() );
if( !clipped )
continue;
BOOST_CHECK_PREDICATE( GEOM_TEST::SegmentsHaveSameEndPoints,
( *clipped )( *c.ExpectedClippedSeg ) );
}
}
BOOST_AUTO_TEST_SUITE_END();

32
qa/tests/libs/kimath/math/test_box2.cpp

@ -30,11 +30,43 @@
// Code under test
#include <math/box2.h>
/**
* Declare the test suite
*/
BOOST_AUTO_TEST_SUITE( BOX2TESTS )
BOOST_AUTO_TEST_CASE( DefaultConstructor )
{
const BOX2I box;
BOOST_TEST( box.GetPosition() == VECTOR2I( 0, 0 ) );
BOOST_TEST( box.GetSize() == VECTOR2I( 0, 0 ) );
}
BOOST_AUTO_TEST_CASE( Basic )
{
const BOX2I box( VECTOR2I( 1, 2 ), VECTOR2I( 3, 4 ) );
BOOST_TEST( box.GetPosition() == VECTOR2I( 1, 2 ) );
BOOST_TEST( box.GetSize() == VECTOR2I( 3, 4 ) );
// Check the equality operator
BOOST_CHECK( box == BOX2I( VECTOR2I( 1, 2 ), VECTOR2I( 3, 4 ) ) );
// Inflate
const BOX2I inflated = BOX2I( box ).Inflate( 1 );
BOOST_TEST( inflated.GetPosition() == VECTOR2I( 0, 1 ) );
BOOST_TEST( inflated.GetSize() == VECTOR2I( 5, 6 ) );
}
BOOST_AUTO_TEST_CASE( ByCorners )
{
const BOX2I boxByCorners = BOX2I::ByCorners( VECTOR2I( 1, 2 ), VECTOR2I( 3, 4 ) );
const BOX2I boxByPosSize = BOX2I( VECTOR2I( 1, 2 ), VECTOR2I( 2, 2 ) );
BOOST_CHECK( boxByCorners == boxByPosSize );
}
BOOST_AUTO_TEST_CASE( test_closest_point_to, *boost::unit_test::tolerance( 0.000001 ) )
{
BOX2D box( VECTOR2D( 1, 2 ), VECTOR2D( 3, 4 ) );

Loading…
Cancel
Save