mirror of
https://gitlab.com/kicad/code/kicad.git
synced 2025-04-14 19:19:37 +00:00
Optimize optimizeZoneToZoneAnchors.
Only do tests between 3 polygon pairs with closest bbox centers.
Only do tests between 5 parts of line chain pairs with closest bbox centers.
Gets OptimizeRNEdges down to 350 ms
See https://gitlab.com/kicad/code/kicad/-/issues/18148
(cherry picked from commit 8532e1f9ec
)
Fixes https://gitlab.com/kicad/code/kicad/-/issues/19872
This commit is contained in:
parent
6793e9c519
commit
c73cc2cd79
libs/kimath
pcbnew/ratsnest
@ -81,11 +81,10 @@ struct CLIPPER_Z_VALUE
|
||||
*/
|
||||
class SHAPE_LINE_CHAIN : public SHAPE_LINE_CHAIN_BASE
|
||||
{
|
||||
private:
|
||||
public:
|
||||
typedef std::vector<VECTOR2I>::iterator point_iter;
|
||||
typedef std::vector<VECTOR2I>::const_iterator point_citer;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Represent an intersection between two line segments
|
||||
*/
|
||||
@ -218,6 +217,40 @@ public:
|
||||
virtual bool Collide( const SEG& aSeg, int aClearance = 0, int* aActual = nullptr,
|
||||
VECTOR2I* aLocation = nullptr ) const override;
|
||||
|
||||
/**
|
||||
* Finds closest points between this and the other line chain. Doesn't test segments or arcs.
|
||||
*
|
||||
* @param aOther the line chain to test against.
|
||||
* @param aPt0 closest point on this line chain (output).
|
||||
* @param aPt1 closest point on the other line chain (output).
|
||||
* @param aDistance distance between points (output).
|
||||
* @return true, if the operation was successful.
|
||||
*/
|
||||
bool ClosestPoints( const SHAPE_LINE_CHAIN& aOther, VECTOR2I& aPt0, VECTOR2I& aPt1 ) const;
|
||||
|
||||
static bool ClosestPoints( const point_citer& aMyStart, const point_citer& aMyEnd,
|
||||
const point_citer& aOtherStart, const point_citer& aOtherEnd,
|
||||
VECTOR2I& aPt0, VECTOR2I& aPt1, int64_t& aDistSq );
|
||||
|
||||
static bool ClosestSegments( const VECTOR2I& aMyPrevPt, const point_citer& aMyStart,
|
||||
const point_citer& aMyEnd, const VECTOR2I& aOtherPrevPt,
|
||||
const point_citer& aOtherStart, const point_citer& aOtherEnd,
|
||||
VECTOR2I& aPt0, VECTOR2I& aPt1, int64_t& aDistSq );
|
||||
|
||||
/**
|
||||
* Finds closest points between segments of this and the other line chain. Doesn't guarantee
|
||||
* that the points are the absolute closest (use ClosestSegments for that) as there might
|
||||
* be edge cases, but it is much faster.
|
||||
*
|
||||
* @param aOther the line chain to test against.
|
||||
* @param aPt0 closest point on this line chain (output).
|
||||
* @param aPt1 closest point on the other line chain (output).
|
||||
* @param aDistance distance between points (output).
|
||||
* @return true, if the operation was successful.
|
||||
*/
|
||||
bool ClosestSegmentsFast( const SHAPE_LINE_CHAIN& aOther, VECTOR2I& aPt0,
|
||||
VECTOR2I& aPt1 ) const;
|
||||
|
||||
SHAPE_LINE_CHAIN& operator=( const SHAPE_LINE_CHAIN& ) = default;
|
||||
|
||||
SHAPE* Clone() const override;
|
||||
|
@ -42,6 +42,10 @@ struct BOX2I_MINMAX
|
||||
{
|
||||
}
|
||||
|
||||
BOX2I_MINMAX( const VECTOR2I& aPt ) : BOX2I_MINMAX( aPt.x, aPt.y ) {}
|
||||
|
||||
BOX2I_MINMAX( int aX, int aY ) : m_Left( aX ), m_Top( aY ), m_Right( aX ), m_Bottom( aY ) {}
|
||||
|
||||
BOX2I_MINMAX( const BOX2I& aBox ) :
|
||||
m_Left( aBox.GetLeft() ), m_Top( aBox.GetTop() ), m_Right( aBox.GetRight() ),
|
||||
m_Bottom( aBox.GetBottom() )
|
||||
@ -81,6 +85,31 @@ struct BOX2I_MINMAX
|
||||
return left <= right && top <= bottom;
|
||||
}
|
||||
|
||||
void Merge( const VECTOR2I& aPt )
|
||||
{
|
||||
m_Left = std::min( m_Left, aPt.x );
|
||||
m_Right = std::max( m_Right, aPt.x );
|
||||
m_Top = std::min( m_Top, aPt.y );
|
||||
m_Bottom = std::max( m_Bottom, aPt.y );
|
||||
}
|
||||
|
||||
VECTOR2I GetCenter() const
|
||||
{
|
||||
int cx = ( (int64_t) m_Left + m_Right ) / 2;
|
||||
int cy = ( (int64_t) m_Top + m_Bottom ) / 2;
|
||||
|
||||
return VECTOR2I( cx, cy );
|
||||
}
|
||||
|
||||
double GetDiameter() const
|
||||
{
|
||||
VECTOR2L start( m_Left, m_Top );
|
||||
VECTOR2L end( m_Right, m_Bottom );
|
||||
VECTOR2L d = end - start;
|
||||
|
||||
return std::hypot( d.x, d.y );
|
||||
}
|
||||
|
||||
int m_Left;
|
||||
int m_Top;
|
||||
int m_Right;
|
||||
|
@ -36,6 +36,7 @@
|
||||
#include <geometry/shape_line_chain.h>
|
||||
#include <geometry/shape_poly_set.h>
|
||||
#include <math/box2.h> // for BOX2I
|
||||
#include <math/box2_minmax.h>
|
||||
#include <math/util.h> // for rescale
|
||||
#include <math/vector2d.h> // for VECTOR2, VECTOR2I
|
||||
#include <trigo.h> // for RotatePoint
|
||||
@ -547,6 +548,262 @@ void SHAPE_LINE_CHAIN::Rotate( const EDA_ANGLE& aAngle, const VECTOR2I& aCenter
|
||||
}
|
||||
|
||||
|
||||
bool SHAPE_LINE_CHAIN::ClosestSegmentsFast( const SHAPE_LINE_CHAIN& aOther, VECTOR2I& aPt0,
|
||||
VECTOR2I& aPt1 ) const
|
||||
{
|
||||
const std::vector<VECTOR2I>& myPts = m_points;
|
||||
const std::vector<VECTOR2I>& otherPts = aOther.m_points;
|
||||
|
||||
const int c_maxBoxes = 100;
|
||||
const int c_minPtsPerBox = 20;
|
||||
|
||||
int myPointsPerBox = std::max( c_minPtsPerBox, int( myPts.size() / c_maxBoxes ) + 1 );
|
||||
int otherPointsPerBox = std::max( c_minPtsPerBox, int( otherPts.size() / c_maxBoxes ) + 1 );
|
||||
|
||||
int myNumBoxes = ( myPts.size() + myPointsPerBox - 1 ) / myPointsPerBox;
|
||||
int otherNumBoxes = ( otherPts.size() + otherPointsPerBox - 1 ) / otherPointsPerBox;
|
||||
|
||||
struct BOX
|
||||
{
|
||||
BOX2I_MINMAX bbox;
|
||||
VECTOR2I center;
|
||||
int radius;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
std::vector<BOX> myBoxes( myNumBoxes );
|
||||
std::vector<BOX> otherBoxes( otherNumBoxes );
|
||||
|
||||
// Calculate bounding boxes
|
||||
for( size_t i = 0; i < myPts.size(); i++ )
|
||||
{
|
||||
const VECTOR2I pt = myPts[i];
|
||||
BOX& box = myBoxes[i / myPointsPerBox];
|
||||
|
||||
if( box.valid )
|
||||
{
|
||||
box.bbox.Merge( pt );
|
||||
}
|
||||
else
|
||||
{
|
||||
box.bbox = BOX2I_MINMAX( pt );
|
||||
box.valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
for( size_t i = 0; i < otherPts.size(); i++ )
|
||||
{
|
||||
const VECTOR2I pt = otherPts[i];
|
||||
BOX& box = otherBoxes[i / otherPointsPerBox];
|
||||
|
||||
if( box.valid )
|
||||
{
|
||||
box.bbox.Merge( pt );
|
||||
}
|
||||
else
|
||||
{
|
||||
box.bbox = BOX2I_MINMAX( pt );
|
||||
box.valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Store centers and radiuses
|
||||
for( BOX& box : myBoxes )
|
||||
{
|
||||
box.center = box.bbox.GetCenter();
|
||||
box.radius = int( box.bbox.GetDiameter() / 2 );
|
||||
}
|
||||
|
||||
for( BOX& box : otherBoxes )
|
||||
{
|
||||
box.center = box.bbox.GetCenter();
|
||||
box.radius = int( box.bbox.GetDiameter() / 2 );
|
||||
}
|
||||
|
||||
// Find closest pairs
|
||||
struct DIST_PAIR
|
||||
{
|
||||
DIST_PAIR( int64_t aDistSq, size_t aIdA, size_t aIdB ) :
|
||||
dist( aDistSq ), idA( aIdA ), idB( aIdB )
|
||||
{
|
||||
}
|
||||
|
||||
int64_t dist;
|
||||
size_t idA;
|
||||
size_t idB;
|
||||
};
|
||||
|
||||
std::vector<DIST_PAIR> pairsToTest;
|
||||
|
||||
for( size_t ia = 0; ia < myBoxes.size(); ia++ )
|
||||
{
|
||||
for( size_t ib = 0; ib < otherBoxes.size(); ib++ )
|
||||
{
|
||||
const BOX& ca = myBoxes[ia];
|
||||
const BOX& cb = otherBoxes[ib];
|
||||
|
||||
if( !ca.valid || !cb.valid )
|
||||
continue;
|
||||
|
||||
VECTOR2L pA( ca.center );
|
||||
VECTOR2L pB( cb.center );
|
||||
|
||||
int64_t dist = ( pB - pA ).EuclideanNorm();
|
||||
|
||||
dist -= ca.radius;
|
||||
dist -= cb.radius;
|
||||
|
||||
pairsToTest.emplace_back( dist, ia, ib );
|
||||
}
|
||||
}
|
||||
|
||||
std::sort( pairsToTest.begin(), pairsToTest.end(),
|
||||
[]( const DIST_PAIR& a, const DIST_PAIR& b )
|
||||
{
|
||||
return a.dist < b.dist;
|
||||
} );
|
||||
|
||||
const int c_polyPairsLimit = 5;
|
||||
|
||||
// Find closest segments in tested pairs
|
||||
int64_t total_closest_dist_sq = VECTOR2I::ECOORD_MAX;
|
||||
|
||||
for( size_t pairId = 0; pairId < pairsToTest.size() && pairId < c_polyPairsLimit; pairId++ )
|
||||
{
|
||||
const DIST_PAIR& pair = pairsToTest[pairId];
|
||||
|
||||
VECTOR2I ptA;
|
||||
VECTOR2I ptB;
|
||||
int64_t dist_sq;
|
||||
|
||||
size_t myStartId = pair.idA * myPointsPerBox;
|
||||
size_t myEndId = myStartId + myPointsPerBox;
|
||||
|
||||
if( myEndId > myPts.size() )
|
||||
myEndId = myPts.size();
|
||||
|
||||
VECTOR2I myPrevPt = myPts[myStartId == 0 ? myPts.size() - 1 : myStartId - 1];
|
||||
|
||||
size_t otherStartId = pair.idB * otherPointsPerBox;
|
||||
size_t otherEndId = otherStartId + otherPointsPerBox;
|
||||
|
||||
if( otherEndId > otherPts.size() )
|
||||
otherEndId = otherPts.size();
|
||||
|
||||
VECTOR2I otherPrevPt = otherPts[otherStartId == 0 ? otherPts.size() - 1 : otherStartId - 1];
|
||||
|
||||
if( ClosestSegments( myPrevPt, myPts.begin() + myStartId, myPts.begin() + myEndId,
|
||||
otherPrevPt, otherPts.begin() + otherStartId,
|
||||
otherPts.begin() + otherEndId, ptA, ptB, dist_sq ) )
|
||||
{
|
||||
if( dist_sq < total_closest_dist_sq )
|
||||
{
|
||||
total_closest_dist_sq = dist_sq;
|
||||
aPt0 = ptA;
|
||||
aPt1 = ptB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return total_closest_dist_sq != VECTOR2I::ECOORD_MAX;
|
||||
}
|
||||
|
||||
|
||||
bool SHAPE_LINE_CHAIN::ClosestSegments( const VECTOR2I& aMyPrevPt, const point_citer& aMyStart,
|
||||
const point_citer& aMyEnd, const VECTOR2I& aOtherPrevPt,
|
||||
const point_citer& aOtherStart,
|
||||
const point_citer& aOtherEnd, VECTOR2I& aPt0,
|
||||
VECTOR2I& aPt1, int64_t& aDistSq )
|
||||
{
|
||||
if( aMyStart == aMyEnd )
|
||||
return false;
|
||||
|
||||
if( aOtherStart == aOtherEnd )
|
||||
return false;
|
||||
|
||||
int64_t closest_dist_sq = VECTOR2I::ECOORD_MAX;
|
||||
VECTOR2I lastPtA = aMyPrevPt;
|
||||
|
||||
for( point_citer itA = aMyStart; itA != aMyEnd; itA++ )
|
||||
{
|
||||
const VECTOR2I& ptA = *itA;
|
||||
VECTOR2I lastPtB = aOtherPrevPt;
|
||||
|
||||
for( point_citer itB = aOtherStart; itB != aOtherEnd; itB++ )
|
||||
{
|
||||
const VECTOR2I& ptB = *itB;
|
||||
|
||||
SEG segA( lastPtA, ptA );
|
||||
SEG segB( lastPtB, ptB );
|
||||
|
||||
VECTOR2I nearestA, nearestB;
|
||||
|
||||
int64_t dist_sq;
|
||||
|
||||
if( segA.NearestPoints( segB, nearestA, nearestB, dist_sq ) )
|
||||
{
|
||||
if( dist_sq < closest_dist_sq )
|
||||
{
|
||||
closest_dist_sq = dist_sq;
|
||||
aPt0 = nearestA;
|
||||
aPt1 = nearestB;
|
||||
}
|
||||
}
|
||||
|
||||
lastPtB = ptB;
|
||||
}
|
||||
|
||||
lastPtA = ptA;
|
||||
}
|
||||
|
||||
aDistSq = closest_dist_sq;
|
||||
return closest_dist_sq != VECTOR2I::ECOORD_MAX;
|
||||
}
|
||||
|
||||
|
||||
bool SHAPE_LINE_CHAIN::ClosestPoints( const point_citer& aMyStart, const point_citer& aMyEnd,
|
||||
const point_citer& aOtherStart, const point_citer& aOtherEnd,
|
||||
VECTOR2I& aPt0, VECTOR2I& aPt1, int64_t& aDistSq )
|
||||
{
|
||||
int64_t closest_dist_sq = VECTOR2I::ECOORD_MAX;
|
||||
|
||||
for( point_citer itA = aMyStart; itA != aMyEnd; itA++ )
|
||||
{
|
||||
const VECTOR2I& ptA = *itA;
|
||||
|
||||
for( point_citer itB = aOtherStart; itB != aOtherEnd; itB++ )
|
||||
{
|
||||
const VECTOR2I& ptB = *itB;
|
||||
|
||||
ecoord dx = (ecoord) ptB.x - ptA.x;
|
||||
ecoord dy = (ecoord) ptB.y - ptA.y;
|
||||
|
||||
SEG::ecoord dist_sq = dx * dx + dy * dy;
|
||||
|
||||
if( dist_sq < closest_dist_sq )
|
||||
{
|
||||
closest_dist_sq = dist_sq;
|
||||
aPt0 = ptA;
|
||||
aPt1 = ptB;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
aDistSq = closest_dist_sq;
|
||||
return closest_dist_sq != VECTOR2I::ECOORD_MAX;
|
||||
}
|
||||
|
||||
|
||||
bool SHAPE_LINE_CHAIN::ClosestPoints( const SHAPE_LINE_CHAIN& aOther, VECTOR2I& aPt0,
|
||||
VECTOR2I& aPt1 ) const
|
||||
{
|
||||
ecoord dist_sq;
|
||||
|
||||
return ClosestPoints( m_points.cbegin(), m_points.cend(), aOther.m_points.cbegin(),
|
||||
aOther.m_points.cend(), aPt0, aPt1, dist_sq );
|
||||
}
|
||||
|
||||
|
||||
bool SHAPE_LINE_CHAIN_BASE::Collide( const SEG& aSeg, int aClearance, int* aActual,
|
||||
VECTOR2I* aLocation ) const
|
||||
{
|
||||
|
@ -392,47 +392,111 @@ void RN_NET::OptimizeRNEdges()
|
||||
auto optimizeZoneToZoneAnchors =
|
||||
[&]( const std::shared_ptr<const CN_ANCHOR>& a,
|
||||
const std::shared_ptr<const CN_ANCHOR>& b,
|
||||
const std::function<void(const std::shared_ptr<const CN_ANCHOR>&)>& setOptimizedATo,
|
||||
const std::function<void(const std::shared_ptr<const CN_ANCHOR>&)>& setOptimizedBTo )
|
||||
const std::function<void( const std::shared_ptr<const CN_ANCHOR>& )>&
|
||||
setOptimizedATo,
|
||||
const std::function<void( const std::shared_ptr<const CN_ANCHOR>& )>&
|
||||
setOptimizedBTo )
|
||||
{
|
||||
struct CENTER
|
||||
{
|
||||
VECTOR2I pt;
|
||||
bool valid = false;
|
||||
};
|
||||
|
||||
struct DIST_PAIR
|
||||
{
|
||||
DIST_PAIR( int64_t aDistSq, size_t aIdA, size_t aIdB )
|
||||
: dist_sq( aDistSq ), idA( aIdA ), idB( aIdB )
|
||||
{}
|
||||
|
||||
int64_t dist_sq;
|
||||
size_t idA;
|
||||
size_t idB;
|
||||
};
|
||||
|
||||
const std::vector<CN_ITEM*>& connectedItemsA = a->Item()->ConnectedItems();
|
||||
const std::vector<CN_ITEM*>& connectedItemsB = b->Item()->ConnectedItems();
|
||||
|
||||
std::vector<CENTER> centersA( connectedItemsA.size() );
|
||||
std::vector<CENTER> centersB( connectedItemsB.size() );
|
||||
|
||||
for( size_t i = 0; i < connectedItemsA.size(); i++ )
|
||||
{
|
||||
CN_ITEM* itemA = connectedItemsA[i];
|
||||
CN_ZONE_LAYER* zoneLayerA = dynamic_cast<CN_ZONE_LAYER*>( itemA );
|
||||
|
||||
if( !zoneLayerA )
|
||||
continue;
|
||||
|
||||
const SHAPE_LINE_CHAIN& shapeA = zoneLayerA->GetOutline();
|
||||
centersA[i].pt = shapeA.BBox().GetCenter();
|
||||
centersA[i].valid = true;
|
||||
}
|
||||
|
||||
for( size_t i = 0; i < connectedItemsB.size(); i++ )
|
||||
{
|
||||
CN_ITEM* itemB = connectedItemsB[i];
|
||||
CN_ZONE_LAYER* zoneLayerB = dynamic_cast<CN_ZONE_LAYER*>( itemB );
|
||||
|
||||
if( !zoneLayerB )
|
||||
continue;
|
||||
|
||||
const SHAPE_LINE_CHAIN& shapeB = zoneLayerB->GetOutline();
|
||||
centersB[i].pt = shapeB.BBox().GetCenter();
|
||||
centersB[i].valid = true;
|
||||
}
|
||||
|
||||
std::vector<DIST_PAIR> pairsToTest;
|
||||
|
||||
for( size_t ia = 0; ia < centersA.size(); ia++ )
|
||||
{
|
||||
for( size_t ib = 0; ib < centersB.size(); ib++ )
|
||||
{
|
||||
for( CN_ITEM* itemA : a->Item()->ConnectedItems() )
|
||||
{
|
||||
CN_ZONE_LAYER* zoneLayerA = dynamic_cast<CN_ZONE_LAYER*>( itemA );
|
||||
const CENTER& ca = centersA[ia];
|
||||
const CENTER& cb = centersB[ib];
|
||||
|
||||
if( !zoneLayerA )
|
||||
continue;
|
||||
if( !ca.valid || !cb.valid )
|
||||
continue;
|
||||
|
||||
for( CN_ITEM* itemB : b->Item()->ConnectedItems() )
|
||||
{
|
||||
CN_ZONE_LAYER* zoneLayerB = dynamic_cast<CN_ZONE_LAYER*>( itemB );
|
||||
VECTOR2L pA( ca.pt );
|
||||
VECTOR2L pB( cb.pt );
|
||||
|
||||
if( !zoneLayerB || zoneLayerB == zoneLayerA )
|
||||
continue;
|
||||
int64_t dist_sq = ( pB - pA ).SquaredEuclideanNorm();
|
||||
pairsToTest.emplace_back( dist_sq, ia, ib );
|
||||
}
|
||||
}
|
||||
|
||||
if( zoneLayerB->Layer() == zoneLayerA->Layer() )
|
||||
{
|
||||
// Process the first matching layer. We don't really care if it's
|
||||
// the "best" layer or not, as anything will be better than the
|
||||
// original anchors (which are connected to the zone and so certainly
|
||||
// don't look like they should have ratsnest lines coming off them).
|
||||
std::sort( pairsToTest.begin(), pairsToTest.end(),
|
||||
[]( const DIST_PAIR& _a, const DIST_PAIR& _b )
|
||||
{
|
||||
return _a.dist_sq < _b.dist_sq;
|
||||
} );
|
||||
|
||||
VECTOR2I startA = zoneLayerA->GetOutline().GetPoint( 0 );
|
||||
VECTOR2I startB = zoneLayerB->GetOutline().GetPoint( 0 );
|
||||
const SHAPE* shapeA = &zoneLayerA->GetOutline();
|
||||
const SHAPE* shapeB = &zoneLayerB->GetOutline();
|
||||
int startDist = ( startA - startB ).EuclideanNorm();
|
||||
const int c_polyPairsLimit = 3;
|
||||
|
||||
VECTOR2I ptA;
|
||||
shapeA->Collide( shapeB, startDist + 10, nullptr, &ptA );
|
||||
setOptimizedATo( std::make_shared<CN_ANCHOR>( ptA, zoneLayerA ) );
|
||||
for( size_t i = 0; i < pairsToTest.size() && i < c_polyPairsLimit; i++ )
|
||||
{
|
||||
const DIST_PAIR& pair = pairsToTest[i];
|
||||
|
||||
VECTOR2I ptB;
|
||||
shapeB->Collide( shapeA, startDist + 10, nullptr, &ptB );
|
||||
setOptimizedBTo( std::make_shared<CN_ANCHOR>( ptB, zoneLayerB ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
CN_ZONE_LAYER* zoneLayerA = static_cast<CN_ZONE_LAYER*>( connectedItemsA[pair.idA] );
|
||||
CN_ZONE_LAYER* zoneLayerB = static_cast<CN_ZONE_LAYER*>( connectedItemsB[pair.idB] );
|
||||
|
||||
if( zoneLayerA == zoneLayerB )
|
||||
continue;
|
||||
|
||||
const SHAPE_LINE_CHAIN& shapeA = zoneLayerA->GetOutline();
|
||||
const SHAPE_LINE_CHAIN& shapeB = zoneLayerB->GetOutline();
|
||||
|
||||
VECTOR2I ptA;
|
||||
VECTOR2I ptB;
|
||||
|
||||
if( shapeA.ClosestSegmentsFast( shapeB, ptA, ptB ) )
|
||||
{
|
||||
setOptimizedATo( std::make_shared<CN_ANCHOR>( ptA, zoneLayerA ) );
|
||||
setOptimizedBTo( std::make_shared<CN_ANCHOR>( ptB, zoneLayerB ) );
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for( CN_EDGE& edge : m_rnEdges )
|
||||
{
|
||||
|
Loading…
Reference in New Issue
Block a user