7
mirror of https://gitlab.com/kicad/code/kicad.git synced 2025-04-21 00:21:25 +00:00

Pcbnew: Snap to graphic/track intersections

Relates-To: https://gitlab.com/kicad/code/kicad/-/issues/2329
This commit is contained in:
John Beard 2024-08-13 11:52:01 +01:00
parent 6c5c9fe89a
commit 1fb2d7fe26
12 changed files with 561 additions and 17 deletions

View File

@ -110,6 +110,20 @@ static void DrawQuadrantPointIcon( GAL& aGal, const VECTOR2I& aPosition, int aSi
EDA_ANGLE( 140, EDA_ANGLE_T::DEGREES_T ) );
}
static void DrawIntersectionIcon( GAL& aGal, const VECTOR2I& aPosition, int aSize )
{
const int nodeRadius = aSize / 8;
DrawSnapNode( aGal, aPosition, nodeRadius );
// Slightly squashed X shape
VECTOR2I xLeg = VECTOR2I( aSize / 2, aSize / 3 );
aGal.DrawLine( aPosition - xLeg, aPosition + xLeg );
xLeg.y = -xLeg.y;
aGal.DrawLine( aPosition - xLeg, aPosition + xLeg );
}
void SNAP_INDICATOR::ViewDraw( int, VIEW* aView ) const
{
@ -155,4 +169,8 @@ void SNAP_INDICATOR::ViewDraw( int, VIEW* aView ) const
{
DrawQuadrantPointIcon( gal, typeIconPos, size );
}
else if( m_snapTypes & POINT_TYPE::PT_INTERSECTION )
{
DrawIntersectionIcon( gal, typeIconPos, size );
}
}

View File

@ -0,0 +1,47 @@
/*
* 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
/**
* @brief A type that is always false.
*
* Useful for static_asserts, when 'false' is no good as the
* compiler likes to believe that the assert could pass in
* some cases:
*
* if constexpr ( something<T>() )
* {
* // Say we know that this branch must always be chosen.
* }
* else
* {
* // Even if the above if's are made such this branch is never chosen
* // compilers may still complain if this were to use a literal 'false'.
* static_assert( always_false<T>::value, "This should never happen" );
* }
*/
template <typename T>
struct always_false : std::false_type
{
};

View File

@ -19,6 +19,7 @@ set( KIMATH_SRCS
src/geometry/convex_hull.cpp
src/geometry/direction_45.cpp
src/geometry/geometry_utils.cpp
src/geometry/intersection.cpp
src/geometry/oval.cpp
src/geometry/seg.cpp
src/geometry/shape.cpp

View File

@ -0,0 +1,73 @@
/*
* 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>
class SEG;
class CIRCLE;
class SHAPE_ARC;
class SHAPE_RECT;
/**
* 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>;
/**
* A visitor that visits INTERSECTABLE_GEOM variant objects with another
* (which is held as state: m_otherGeometry).
*
* This provides a unified way to intersect any supported geometry with
* any other supported geometry.
*/
struct INTERSECTION_VISITOR
{
public:
/**
* @param aOtherGeometry The other geometry to intersect the visited geometry with.
* @param aIntersections A vector to store the intersections in. Does not have to
* be empty, the visitor will append to it.
*/
INTERSECTION_VISITOR( const INTERSECTABLE_GEOM& aOtherGeometry,
std::vector<VECTOR2I>& aIntersections );
/*
* One of these operator() overloads will be called by std::visit
* as needed to visit (i.e. intersect) the geometry with the (stored)
* other geometry.
*/
void operator()( const SEG& aSeg ) const;
void operator()( const CIRCLE& aCircle ) const;
void operator()( const SHAPE_ARC& aArc ) const;
void operator()( const SHAPE_RECT& aArc ) const;
private:
const INTERSECTABLE_GEOM& m_otherGeometry;
std::vector<VECTOR2I>& m_intersections;
};

View File

@ -61,6 +61,10 @@ enum POINT_TYPE
* (you may want to infer PT_END from this)
*/
PT_CORNER = 1 << 4,
/**
* The point is an intersection of two (or more) items.
*/
PT_INTERSECTION = 1 << 5,
};
struct TYPED_POINT2I

View File

@ -31,6 +31,7 @@
#include <math/vector2d.h> // for VECTOR2I
#include <geometry/eda_angle.h>
class CIRCLE;
class SHAPE_LINE_CHAIN;
class SHAPE_ARC : public SHAPE
@ -142,9 +143,18 @@ public:
int IntersectLine( const SEG& aSeg, std::vector<VECTOR2I>* aIpsBuffer ) const;
/**
* Find intersection points between this arc and aArc. Ignores arc width.
* Find intersection points between this arc and a CIRCLE. Ignores arc width.
*
* @param aSeg
* @param aCircle Circle to intersect against
* @param aIpsBuffer Buffer to store the resulting intersection points (if any)
* @return Number of intersection points found
*/
int Intersect( const CIRCLE& aArc, std::vector<VECTOR2I>* aIpsBuffer ) const;
/**
* Find intersection points between this arc and another arc. Ignores arc width.
*
* @param aArc Arc to intersect against
* @param aIpsBuffer Buffer to store the resulting intersection points (if any)
* @return Number of intersection points found
*/

View File

@ -76,6 +76,16 @@ public:
m_h( aH )
{}
/**
* Create by two corners.
*/
SHAPE_RECT( const VECTOR2I& aP0, const VECTOR2I& aP1 ) :
SHAPE( SH_RECT ),
m_p0( aP0 ),
m_w( aP1.x - aP0.x ),
m_h( aP1.y - aP0.y )
{}
SHAPE_RECT( const SHAPE_RECT& aOther ) :
SHAPE( SH_RECT ),
m_p0( aOther.m_p0 ),

View File

@ -0,0 +1,233 @@
/*
* 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/intersection.h"
#include <core/type_helpers.h>
#include <geometry/seg.h>
#include <geometry/circle.h>
#include <geometry/shape_arc.h>
#include <geometry/shape_rect.h>
/*
* Helper functions that dispatch to the correct intersection function
* in one of the geometry classes.
*/
namespace
{
void findIntersections( const SEG& aSegA, const SEG& aSegB, std::vector<VECTOR2I>& aIntersections )
{
const OPT_VECTOR2I intersection = aSegA.Intersect( aSegB );
if( intersection )
{
aIntersections.push_back( *intersection );
}
}
void findIntersections( const SEG& aSeg, const CIRCLE& aCircle,
std::vector<VECTOR2I>& aIntersections )
{
std::vector<VECTOR2I> intersections = aCircle.Intersect( aSeg );
aIntersections.insert( aIntersections.end(), intersections.begin(), intersections.end() );
}
void findIntersections( const SEG& aSeg, const SHAPE_ARC& aArc,
std::vector<VECTOR2I>& aIntersections )
{
std::vector<VECTOR2I> intersections;
aArc.IntersectLine( aSeg, &intersections );
// Find only the intersections that are within the segment
for( const VECTOR2I& intersection : intersections )
{
if( aSeg.Contains( intersection ) )
{
aIntersections.emplace_back( intersection );
}
}
}
void findIntersections( const CIRCLE& aCircleA, const CIRCLE& aCircleB,
std::vector<VECTOR2I>& aIntersections )
{
std::vector<VECTOR2I> intersections = aCircleA.Intersect( aCircleB );
aIntersections.insert( aIntersections.end(), intersections.begin(), intersections.end() );
}
void findIntersections( const CIRCLE& aCircle, const SHAPE_ARC& aArc,
std::vector<VECTOR2I>& aIntersections )
{
aArc.Intersect( aCircle, &aIntersections );
}
void findIntersections( const SHAPE_ARC& aArcA, const SHAPE_ARC& aArcB,
std::vector<VECTOR2I>& aIntersections )
{
aArcA.Intersect( aArcB, &aIntersections );
}
std::vector<SEG> RectToSegs( const SHAPE_RECT& aRect )
{
const VECTOR2I corner = aRect.GetPosition();
const int w = aRect.GetWidth();
const int h = aRect.GetHeight();
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 ),
};
}
} // namespace
INTERSECTION_VISITOR::INTERSECTION_VISITOR( const INTERSECTABLE_GEOM& aOtherGeometry,
std::vector<VECTOR2I>& aIntersections ) :
m_otherGeometry( aOtherGeometry ), m_intersections( aIntersections )
{
}
/*
* The operator() functions are the entry points for the visitor.
*
* Dispatch to the correct function based on the type of the "otherGeometry"
* which is held as state. This is also where the order of the parameters is
* determined, which avoids having to define a 'reverse' function for each
* intersection type.
*/
void INTERSECTION_VISITOR::operator()( const SEG& aSeg ) const
{
// Dispatch to the correct function
return std::visit(
[&]( const auto& otherGeom )
{
using OtherGeomType = std::decay_t<decltype( otherGeom )>;
if constexpr( std::is_same_v<OtherGeomType, SHAPE_RECT> )
{
// Seg-Rect via decomposition into segments
for( const SEG& aRectSeg : RectToSegs( otherGeom ) )
{
findIntersections( aSeg, aRectSeg, m_intersections );
}
}
else
{
// In all other segment comparisons, the SEG is the first argument
findIntersections( aSeg, otherGeom, m_intersections );
}
},
m_otherGeometry );
}
void INTERSECTION_VISITOR::operator()( const CIRCLE& aCircle ) 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, CIRCLE> )
{
// Seg-Circle, Circle-Circle
findIntersections( otherGeom, aCircle, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, SHAPE_ARC> )
{
// Circle-Arc
findIntersections( aCircle, otherGeom, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, SHAPE_RECT> )
{
// Circle-Rect via decomposition into segments
for( const SEG& aRectSeg : RectToSegs( otherGeom ) )
{
findIntersections( aRectSeg, aCircle, m_intersections );
}
}
else
{
static_assert( always_false<OtherGeomType>::value,
"Unhandled other geometry type" );
}
},
m_otherGeometry );
}
void INTERSECTION_VISITOR::operator()( const SHAPE_ARC& aArc ) 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, CIRCLE>
|| std::is_same_v<OtherGeomType, SHAPE_ARC> )
{
// Seg-Arc, Circle-Arc, Arc-Arc
findIntersections( otherGeom, aArc, m_intersections );
}
else if constexpr( std::is_same_v<OtherGeomType, SHAPE_RECT> )
{
// Arc-Rect via decomposition into segments
for( const SEG& aRectSeg : RectToSegs( otherGeom ) )
{
findIntersections( aRectSeg, aArc, m_intersections );
}
}
else
{
static_assert( always_false<OtherGeomType>::value,
"Unhandled other geometry type" );
}
},
m_otherGeometry );
};
void INTERSECTION_VISITOR::operator()( const SHAPE_RECT& 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 );
for( const SEG& seg : segs )
{
( *this )( seg );
}
};

View File

@ -301,7 +301,25 @@ int SHAPE_ARC::IntersectLine( const SEG& aSeg, std::vector<VECTOR2I>* aIpsBuffer
std::vector<VECTOR2I> intersections = circ.IntersectLine( aSeg );
size_t originalSize = aIpsBuffer->size();
const size_t originalSize = aIpsBuffer->size();
for( const VECTOR2I& intersection : intersections )
{
if( sliceContainsPoint( intersection ) )
aIpsBuffer->push_back( intersection );
}
return aIpsBuffer->size() - originalSize;
}
int SHAPE_ARC::Intersect( const CIRCLE& aCircle, std::vector<VECTOR2I>* aIpsBuffer ) const
{
CIRCLE thiscirc( GetCenter(), GetRadius() );
std::vector<VECTOR2I> intersections = thiscirc.Intersect( aCircle );
const size_t originalSize = aIpsBuffer->size();
for( const VECTOR2I& intersection : intersections )
{
@ -320,7 +338,7 @@ int SHAPE_ARC::Intersect( const SHAPE_ARC& aArc, std::vector<VECTOR2I>* aIpsBuff
std::vector<VECTOR2I> intersections = thiscirc.Intersect( othercirc );
size_t originalSize = aIpsBuffer->size();
const size_t originalSize = aIpsBuffer->size();
for( const VECTOR2I& intersection : intersections )
{

View File

@ -35,6 +35,7 @@
#include <pcb_track.h>
#include <zone.h>
#include <gal/graphics_abstraction_layer.h>
#include <geometry/intersection.h>
#include <geometry/oval.h>
#include <geometry/shape_circle.h>
#include <geometry/shape_line_chain.h>
@ -195,7 +196,7 @@ VECTOR2I PCB_GRID_HELPER::AlignToNearestPad( const VECTOR2I& aMousePos, std::deq
clearAnchors();
for( BOARD_ITEM* item : aPads )
computeAnchors( item, aMousePos, true );
computeAnchors( item, aMousePos, true, nullptr );
double minDist = std::numeric_limits<double>::max();
ANCHOR* nearestOrigin = nullptr;
@ -230,8 +231,7 @@ VECTOR2I PCB_GRID_HELPER::BestDragOrigin( const VECTOR2I &aMousePos,
{
clearAnchors();
for( BOARD_ITEM* item : aItems )
computeAnchors( item, aMousePos, true, aSelectionFilter );
computeAnchors( aItems, aMousePos, true, aSelectionFilter );
double worldScale = m_toolMgr->GetView()->GetGAL()->GetWorldScale();
double lineSnapMinCornerDistance = 50.0 / worldScale;
@ -315,8 +315,8 @@ VECTOR2I PCB_GRID_HELPER::BestSnapAnchor( const VECTOR2I& aOrigin, const LSET& a
clearAnchors();
for( BOARD_ITEM* item : queryVisible( bb, aSkip ) )
computeAnchors( item, aOrigin );
const std::vector<BOARD_ITEM*> visibleItems = queryVisible( bb, aSkip );
computeAnchors( visibleItems, aOrigin, false, nullptr );
ANCHOR* nearest = nearestAnchor( aOrigin, SNAPPABLE, aLayers );
VECTOR2I nearestGrid = Align( aOrigin, aGrid );
@ -480,8 +480,8 @@ VECTOR2D PCB_GRID_HELPER::GetGridSize( GRID_HELPER_GRIDS aGrid ) const
}
std::set<BOARD_ITEM*> PCB_GRID_HELPER::queryVisible( const BOX2I& aArea,
const std::vector<BOARD_ITEM*>& aSkip ) const
std::vector<BOARD_ITEM*>
PCB_GRID_HELPER::queryVisible( const BOX2I& aArea, const std::vector<BOARD_ITEM*>& aSkip ) const
{
std::set<BOARD_ITEM*> items;
std::vector<KIGFX::VIEW::LAYER_ITEM_PAIR> selectedItems;
@ -538,7 +538,127 @@ std::set<BOARD_ITEM*> PCB_GRID_HELPER::queryVisible( const BOX2I& aArea,
for( BOARD_ITEM* item : aSkip )
skipItem( item );
return items;
return {items.begin(), items.end()};
}
struct PCB_INTERSECTABLE
{
BOARD_ITEM* Item;
INTERSECTABLE_GEOM Geometry;
// Clang wants this constructor
PCB_INTERSECTABLE( BOARD_ITEM* aItem, INTERSECTABLE_GEOM aSeg ) :
Item( aItem ), Geometry( std::move( aSeg ) )
{
}
};
void PCB_GRID_HELPER::computeAnchors( const std::vector<BOARD_ITEM*>& aItems,
const VECTOR2I& aRefPos, bool aFrom,
const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter )
{
std::vector<PCB_INTERSECTABLE> intersectables;
// This c/should come from a more granular snap filter
const bool computeIntersections = true;
const bool excludeGraphics = aSelectionFilter && !aSelectionFilter->graphics;
const bool excludeTracks = aSelectionFilter && !aSelectionFilter->tracks;
for( BOARD_ITEM* item : aItems )
{
// First, add all the key points of the item itself
computeAnchors( item, aRefPos, aFrom, aSelectionFilter );
// If we are computing intersections, construct the relevant intersectables
if( computeIntersections )
{
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;
}
}
else if( !excludeTracks )
{
switch( item->Type() )
{
case PCB_TRACE_T:
{
PCB_TRACK& track = static_cast<PCB_TRACK&>( *item );
intersectables.emplace_back( &track, SEG{ track.GetStart(), track.GetEnd() } );
break;
}
case PCB_ARC_T:
{
PCB_ARC& arc = static_cast<PCB_ARC&>( *item );
intersectables.emplace_back(
&arc, SHAPE_ARC{ arc.GetStart(), arc.GetMid(), arc.GetEnd(), 0 } );
break;
}
default:
// Ignore other items
break;
}
}
}
}
// 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
for( size_t ii = 0; ii < intersectables.size(); ++ii )
{
std::vector<VECTOR2I> intersections;
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];
std::visit( visitor, intersectableB.Geometry );
}
// For each intersection, add an intersection snap anchor
for( const VECTOR2I& intersection : intersections )
{
addAnchor( intersection, SNAPPABLE, intersectableA.Item, POINT_TYPE::PT_INTERSECTION );
}
}
}
@ -1045,7 +1165,7 @@ void PCB_GRID_HELPER::computeAnchors( BOARD_ITEM* aItem, const VECTOR2I& aRefPos
for( BOARD_ITEM* item : static_cast<const PCB_GROUP*>( aItem )->GetItems() )
{
if( checkVisibility( item ) )
computeAnchors( item, aRefPos, aFrom );
computeAnchors( item, aRefPos, aFrom, nullptr );
}
break;

View File

@ -95,11 +95,20 @@ public:
private:
std::set<BOARD_ITEM*> queryVisible( const BOX2I& aArea,
const std::vector<BOARD_ITEM*>& aSkip ) const;
std::vector<BOARD_ITEM*> queryVisible( const BOX2I& aArea,
const std::vector<BOARD_ITEM*>& aSkip ) const;
ANCHOR* nearestAnchor( const VECTOR2I& aPos, int aFlags, LSET aMatchLayers );
/**
* computeAnchors inserts the local anchor points in to the grid helper for the specified
* container of board items, including points implied by intersections or other relationships
* between the items.
*/
void computeAnchors( const std::vector<BOARD_ITEM*>& aItems, const VECTOR2I& aRefPos,
bool aFrom, const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter );
/**
* computeAnchors inserts the local anchor points in to the grid helper for the specified
* board item, given the reference point and the direction of use for the point.
@ -108,8 +117,8 @@ private:
* @param aRefPos The point for which to compute the anchors (if used by the component)
* @param aFrom Is this for an anchor that is designating a source point (aFrom=true) or not
*/
void computeAnchors( BOARD_ITEM* aItem, const VECTOR2I& aRefPos, bool aFrom = false,
const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter = nullptr );
void computeAnchors( BOARD_ITEM* aItem, const VECTOR2I& aRefPos, bool aFrom,
const PCB_SELECTION_FILTER_OPTIONS* aSelectionFilter );
private:
MAGNETIC_SETTINGS* m_magneticSettings;

View File

@ -26,6 +26,7 @@ std::string toString( const POINT_TYPE& aType )
case PT_MID: return "PT_MID";
case PT_QUADRANT: return "PT_QUADRANT";
case PT_CORNER: return "PT_CORNER";
case PT_INTERSECTION: return "PT_INTERSECTION";
default: return "Unknown POINT_TYPE: " + std::to_string( (int) aType );
}
}