Commit e9713bb2 authored by jean-pierre charras's avatar jean-pierre charras

Pcbnew: Block selection enhancement, from Bug #593997 (whishlist)

1. block created from-left-to-right selects only 100%inside selection objects (as it now does)
2.block created from-right-to-left selects all overlapping objects inside selection
From the patch sent by mathieulj (mathieulj), and some fixes and code cleaning.
parent d5ecafd5
......@@ -320,6 +320,36 @@ bool EDA_RECT::Contains( const EDA_RECT& aRect ) const
}
/* Intersects
* test for a common area between segment and rect.
* return true if at least a common point is found
*/
bool EDA_RECT::Intersects( const wxPoint& aPoint1, const wxPoint& aPoint2 ) const
{
wxPoint point2, point4;
if( Contains( aPoint1 ) || Contains( aPoint2 ) )
return true;
point2.x = GetEnd().x;
point2.y = GetOrigin().y;
point4.x = GetOrigin().x;
point4.y = GetEnd().y;
//Only need to test 3 sides since a straight line cant enter and exit on same side
if( SegmentIntersectsSegment( aPoint1, aPoint2, GetOrigin() , point2 ) )
return true;
if( SegmentIntersectsSegment( aPoint1, aPoint2, point2 , GetEnd() ) )
return true;
if( SegmentIntersectsSegment( aPoint1, aPoint2, GetEnd() , point4 ) )
return true;
return false;
}
/* Intersects
* test for a common area between 2 rect.
* return true if at least a common point is found
......
......@@ -9,6 +9,71 @@
#include <common.h>
#include <math_for_graphics.h>
bool SegmentIntersectsSegment( const wxPoint &a_p1_l1, const wxPoint &a_p2_l1,
const wxPoint &a_p1_l2, const wxPoint &a_p2_l2 )
{
//We are forced to use 64bit ints because the internal units can oveflow 32bit ints when
// multiplied with each other, the alternative would be to scale the units down (i.e. divide
// by a fixed number).
long long dX_a, dY_a, dX_b, dY_b, dX_ab, dY_ab;
long long num_a, num_b, den;
//Test for intersection within the bounds of both line segments using line equations of the
// form:
// x_k(u_k) = u_k * dX_k + x_k(0)
// y_k(u_k) = u_k * dY_k + y_k(0)
// with 0 <= u_k <= 1 and k = [ a, b ]
dX_a = a_p2_l1.x - a_p1_l1.x;
dY_a = a_p2_l1.y - a_p1_l1.y;
dX_b = a_p2_l2.x - a_p1_l2.x;
dY_b = a_p2_l2.y - a_p1_l2.y;
dX_ab = a_p1_l2.x - a_p1_l1.x;
dY_ab = a_p1_l2.y - a_p1_l1.y;
den = dY_a * dX_b - dY_b * dX_a ;
//Check if lines are parallel
if( den == 0 )
return false;
num_a = dY_ab * dX_b - dY_b * dX_ab;
num_b = dY_ab * dX_a - dY_a * dX_ab;
//We wont calculate directly the u_k of the intersection point to avoid floating point
// division but they could be calculated with:
// u_a = (float) num_a / (float) den;
// u_b = (float) num_b / (float) den;
if( den < 0 )
{
den = -den;
num_a = -num_a;
num_b = -num_b;
}
//Test sign( u_a ) and return false if negative
if( num_a < 0 )
return false;
//Test sign( u_b ) and return false if negative
if( num_b < 0 )
return false;
//Test to ensure (u_a <= 1)
if( num_a > den )
return false;
//Test to ensure (u_b <= 1)
if( num_b > den )
return false;
return true;
}
/* Function TestSegmentHit
* test for hit on line segment
* i.e. a reference point is within a given distance from segment
......
......@@ -300,11 +300,25 @@ public:
/**
* Function Intersects
* tests for a common area between rectangles.
*
* @param aRect A rectangle to test intersection with.
* @return bool - true if the argument rectangle intersects this rectangle.
* (i.e. if the 2 rectangles have at least a common point)
*/
bool Intersects( const EDA_RECT& aRect ) const;
/**
* Function Intersects
* tests for a common area between a segment and this rectangle.
*
* @param aPoint1 First point of the segment to test intersection with.
* @param aPoint2 Second point of the segment to test intersection with.
* @return bool - true if the argument segment intersects this rectangle.
* (i.e. if the segment and rectangle have at least a common point)
*/
bool Intersects( const wxPoint& aPoint1, const wxPoint& aPoint2 ) const;
/**
* Function operator(wxRect)
* overloads the cast operator to return a wxRect
......@@ -530,18 +544,6 @@ public:
return false; // derived classes should override this function
}
/**
* Function HitTest
* tests if the \a aRect intersects this object.
* For now, an ending point must be inside \a aRect.
*
* @param aRect A reference to an EDA_RECT object containg the area to test.
* @return True if \a aRect intersects the object, otherwise false.
*/
virtual bool HitTest( const EDA_RECT& aRect ) const
{
return false; // derived classes should override this function
}
/**
* Function GetBoundingBox
......
......@@ -223,6 +223,26 @@ public:
*/
wxString GetLayerName() const;
virtual bool HitTest( const wxPoint& aPosition )
{
return EDA_ITEM::HitTest( aPosition );
}
/**
* Function HitTest
* tests if the \a aRect intersects or contains this object (depending on \a aContained).
*
* @param aRect A reference to an EDA_RECT object containg the area to test.
* @param aContained Test if \a aRect contains this object completly.
* @param aAccuracy Increase the item bounding box by this amount.
* @return bool - True if \a aRect contains this object completly or if \a aRect intersects
* the object and \a aContained is False, otherwise false.
*/
virtual bool HitTest( const EDA_RECT& aRect, bool aContained = true, int aAccuracy = 0) const
{
return false; // derived classes should override this function
}
/**
* Function FormatInternalUnits
......
......@@ -31,6 +31,19 @@
#include <math.h>
#include <wx/gdicmn.h> // For wxPoint
/**
* Function SegmentIntersectsSegment
*
* @param a_p1_l1 The first point of the first line.
* @param a_p2_l1 The second point of the first line.
* @param a_p1_l2 The first point of the second line.
* @param a_p2_l2 The second point of the second line.
* @return bool - true if the two segments defined by four points intersect.
* (i.e. if the 2 segments have at least a common point)
*/
bool SegmentIntersectsSegment( const wxPoint &a_p1_l1, const wxPoint &a_p2_l1,
const wxPoint &a_p1_l2, const wxPoint &a_p2_l2 );
/*
* Calculate the new point of coord coord pX, pY,
* for a rotation center 0, 0, and angle in (1 / 10 degree)
......
......@@ -382,6 +382,7 @@ bool PCB_EDIT_FRAME::HandleBlockEnd( wxDC* DC )
void PCB_EDIT_FRAME::Block_SelectItems()
{
LAYER_MSK layerMask;
bool selectOnlyComplete = GetScreen()->m_BlockLocate.GetWidth() > 0 ;
GetScreen()->m_BlockLocate.Normalize();
......@@ -395,7 +396,7 @@ void PCB_EDIT_FRAME::Block_SelectItems()
{
LAYER_NUM layer = module->GetLayer();
if( module->HitTest( GetScreen()->m_BlockLocate )
if( module->HitTest( GetScreen()->m_BlockLocate, selectOnlyComplete )
&& ( !module->IsLocked() || blockIncludeLockedModules ) )
{
if( blockIncludeItemsOnInvisibleLayers || m_Pcb->IsModuleLayerVisible( layer ) )
......@@ -410,14 +411,14 @@ void PCB_EDIT_FRAME::Block_SelectItems()
// Add tracks and vias
if( blockIncludeTracks )
{
for( TRACK* pt_segm = m_Pcb->m_Track; pt_segm != NULL; pt_segm = pt_segm->Next() )
for( TRACK* track = m_Pcb->m_Track; track != NULL; track = track->Next() )
{
if( pt_segm->HitTest( GetScreen()->m_BlockLocate ) )
if( track->HitTest( GetScreen()->m_BlockLocate, selectOnlyComplete ) )
{
if( blockIncludeItemsOnInvisibleLayers
|| m_Pcb->IsLayerVisible( pt_segm->GetLayer() ) )
|| m_Pcb->IsLayerVisible( track->GetLayer() ) )
{
picker.SetItem ( pt_segm );
picker.SetItem ( track );
itemsList->PushItem( picker );
}
}
......@@ -446,7 +447,7 @@ void PCB_EDIT_FRAME::Block_SelectItems()
if( (GetLayerMask( PtStruct->GetLayer() ) & layerMask) == 0 )
break;
if( !PtStruct->HitTest( GetScreen()->m_BlockLocate ) )
if( !PtStruct->HitTest( GetScreen()->m_BlockLocate, selectOnlyComplete ) )
break;
select_me = true; // This item is in bloc: select it
......@@ -456,7 +457,7 @@ void PCB_EDIT_FRAME::Block_SelectItems()
if( !blockIncludePcbTexts )
break;
if( !PtStruct->HitTest( GetScreen()->m_BlockLocate ) )
if( !PtStruct->HitTest( GetScreen()->m_BlockLocate, selectOnlyComplete ) )
break;
select_me = true; // This item is in bloc: select it
......@@ -466,7 +467,7 @@ void PCB_EDIT_FRAME::Block_SelectItems()
if( ( GetLayerMask( PtStruct->GetLayer() ) & layerMask ) == 0 )
break;
if( !PtStruct->HitTest( GetScreen()->m_BlockLocate ) )
if( !PtStruct->HitTest( GetScreen()->m_BlockLocate, selectOnlyComplete ) )
break;
select_me = true; // This item is in bloc: select it
......@@ -476,7 +477,7 @@ void PCB_EDIT_FRAME::Block_SelectItems()
if( ( GetLayerMask( PtStruct->GetLayer() ) & layerMask ) == 0 )
break;
if( !PtStruct->HitTest( GetScreen()->m_BlockLocate ) )
if( !PtStruct->HitTest( GetScreen()->m_BlockLocate, selectOnlyComplete ) )
break;
select_me = true; // This item is in bloc: select it
......@@ -500,7 +501,7 @@ void PCB_EDIT_FRAME::Block_SelectItems()
{
ZONE_CONTAINER* area = m_Pcb->GetArea( ii );
if( area->HitTest( GetScreen()->m_BlockLocate ) )
if( area->HitTest( GetScreen()->m_BlockLocate, selectOnlyComplete ) )
{
if( blockIncludeItemsOnInvisibleLayers
|| m_Pcb->IsLayerVisible( area->GetLayer() ) )
......
......@@ -430,12 +430,19 @@ bool DIMENSION::HitTest( const wxPoint& aPosition )
}
bool DIMENSION::HitTest( const EDA_RECT& aRect ) const
bool DIMENSION::HitTest( const EDA_RECT& aRect, bool aContained, int aAccuracy ) const
{
if( aRect.Contains( GetPosition() ) )
return true;
EDA_RECT arect = aRect;
arect.Inflate( aAccuracy );
return false;
EDA_RECT rect = GetBoundingBox();
if( aAccuracy )
rect.Inflate( aAccuracy );
if( aContained )
return arect.Contains( rect );
return arect.Intersects( rect );
}
......
......@@ -129,7 +129,10 @@ public:
bool HitTest( const wxPoint& aPosition );
bool HitTest( const EDA_RECT& aRect ) const;
/** @copydoc BOARD_ITEM::HitTest(const EDA_RECT& aRect,
* bool aContained = true, int aAccuracy ) const
*/
bool HitTest( const EDA_RECT& aRect, bool aContained = true, int aAccuracy = 0 ) const;
wxString GetClass() const
{
......
......@@ -494,30 +494,48 @@ bool DRAWSEGMENT::HitTest( const wxPoint& aPosition )
}
bool DRAWSEGMENT::HitTest( const EDA_RECT& aRect ) const
bool DRAWSEGMENT::HitTest( const EDA_RECT& aRect, bool aContained, int aAccuracy ) const
{
wxPoint p1, p2;
int radius;
float theta;
EDA_RECT arect = aRect;
arect.Inflate( aAccuracy );
switch( m_Shape )
{
case S_CIRCLE:
{
int radius = GetRadius();
// Test if area intersects or contains the circle:
if( aContained )
return arect.Contains( GetBoundingBox() );
else
return arect.Intersects( GetBoundingBox() );
break;
// Text if area intersects the circle:
EDA_RECT area = aRect;
area.Inflate( radius );
case S_ARC:
radius = hypot( (double)( GetEnd().x - GetStart().x ),
(double)( GetEnd().y - GetStart().y ) );
theta = std::atan2( GetEnd().y - GetStart().y , GetEnd().x - GetStart().x );
//Approximate the arc with two lines. This should be accurate enough for selection.
p1.x = radius * std::cos( theta + M_PI/4 ) + GetStart().x;
p1.y = radius * std::sin( theta + M_PI/4 ) + GetStart().y;
p2.x = radius * std::cos( theta + M_PI/2 ) + GetStart().x;
p2.y = radius * std::sin( theta + M_PI/2 ) + GetStart().y;
if( aContained )
return arect.Contains( GetEnd() ) && aRect.Contains( p1 ) && aRect.Contains( p2 );
else
return arect.Intersects( GetEnd(), p1 ) || aRect.Intersects( p1, p2 );
if( area.Contains( m_Start ) )
return true;
}
break;
case S_ARC:
case S_SEGMENT:
if( aRect.Contains( GetStart() ) )
return true;
if( aContained )
return arect.Contains( GetStart() ) && aRect.Contains( GetEnd() );
else
return arect.Intersects( GetStart(), GetEnd() );
if( aRect.Contains( GetEnd() ) )
return true;
break;
default:
......
......@@ -172,7 +172,10 @@ public:
virtual bool HitTest( const wxPoint& aPosition );
virtual bool HitTest( const EDA_RECT& aRect ) const;
/** @copydoc BOARD_ITEM::HitTest(const EDA_RECT& aRect,
* bool aContained = true, int aAccuracy ) const
*/
bool HitTest( const EDA_RECT& aRect, bool aContained = true, int aAccuracy = 0 ) const;
wxString GetClass() const
{
......
......@@ -178,10 +178,15 @@ bool PCB_TARGET::HitTest( const wxPoint& aPosition )
}
bool PCB_TARGET::HitTest( const EDA_RECT& aRect ) const
bool PCB_TARGET::HitTest( const EDA_RECT& aRect, bool aContained, int aAccuracy ) const
{
if( aRect.Contains( m_Pos ) )
return true;
EDA_RECT arect = aRect;
arect.Inflate( aAccuracy );
if( aContained )
return arect.Contains( GetBoundingBox() );
else
return GetBoundingBox().Intersects( arect );
return false;
}
......
......@@ -93,7 +93,10 @@ public:
bool HitTest( const wxPoint& aPosition );
bool HitTest( const EDA_RECT& aRect ) const;
/** @copydoc BOARD_ITEM::HitTest(const EDA_RECT& aRect,
* bool aContained = true, int aAccuracy ) const
*/
bool HitTest( const EDA_RECT& aRect, bool aContained = true, int aAccuracy = 0 ) const;
EDA_RECT GetBoundingBox() const;
......
......@@ -555,21 +555,15 @@ bool MODULE::HitTest( const wxPoint& aPosition )
}
bool MODULE::HitTest( const EDA_RECT& aRect ) const
bool MODULE::HitTest( const EDA_RECT& aRect, bool aContained, int aAccuracy ) const
{
if( m_BoundaryBox.GetX() < aRect.GetX() )
return false;
if( m_BoundaryBox.GetY() < aRect.GetY() )
return false;
if( m_BoundaryBox.GetRight() > aRect.GetRight() )
return false;
EDA_RECT arect = aRect;
arect.Inflate( aAccuracy );
if( m_BoundaryBox.GetBottom() > aRect.GetBottom() )
return false;
return true;
if( aContained )
return arect.Contains( m_BoundaryBox );
else
return m_BoundaryBox.Intersects( arect );
}
......
......@@ -318,7 +318,10 @@ public:
bool HitTest( const wxPoint& aPosition );
bool HitTest( const EDA_RECT& aRect ) const;
/** @copydoc BOARD_ITEM::HitTest(const EDA_RECT& aRect,
* bool aContained = true, int aAccuracy ) const
*/
bool HitTest( const EDA_RECT& aRect, bool aContained = true, int aAccuracy = 0 ) const;
/**
* Function GetReference
......
......@@ -71,9 +71,12 @@ public:
return TextHitTest( aPosition );
}
bool HitTest( const EDA_RECT& aRect ) const
/** @copydoc BOARD_ITEM::HitTest(const EDA_RECT& aRect,
* bool aContained = true, int aAccuracy ) const
*/
bool HitTest( const EDA_RECT& aRect, bool aContained = true, int aAccuracy = 0 ) const
{
return TextHitTest( aRect );
return TextHitTest( aRect, aContained, aAccuracy );
}
wxString GetClass() const
......
......@@ -1186,15 +1186,32 @@ bool TRACK::HitTest( const wxPoint& aPosition )
}
bool TRACK::HitTest( const EDA_RECT& aRect ) const
bool TRACK::HitTest( const EDA_RECT& aRect, bool aContained, int aAccuracy ) const
{
if( aRect.Contains( m_Start ) )
return true;
EDA_RECT box;
EDA_RECT arect = aRect;
arect.Inflate( aAccuracy );
if( aRect.Contains( m_End ) )
return true;
if( Type() == PCB_VIA_T )
{
box.SetOrigin( GetStart() );
box.Inflate( GetWidth() >> 1 );
return false;
if(aContained)
return arect.Contains( box );
else
return arect.Intersects( box );
}
else
{
if( aContained )
// Tracks are a specila case:
// they are considered inside the rect if one end
// is inside the rect
return arect.Contains( GetStart() ) || arect.Contains( GetEnd() );
else
return arect.Intersects( GetStart(), GetEnd() );
}
}
......
......@@ -257,7 +257,10 @@ public:
virtual bool HitTest( const wxPoint& aPosition );
virtual bool HitTest( const EDA_RECT& aRect ) const;
/** @copydoc BOARD_ITEM::HitTest(const EDA_RECT& aRect,
* bool aContained = true, int aAccuracy ) const
*/
bool HitTest( const EDA_RECT& aRect, bool aContained = true, int aAccuracy = 0 ) const;
/**
* Function GetVia
......
......@@ -557,25 +557,61 @@ bool ZONE_CONTAINER::HitTestForEdge( const wxPoint& refPos )
}
bool ZONE_CONTAINER::HitTest( const EDA_RECT& aRect ) const
bool ZONE_CONTAINER::HitTest( const EDA_RECT& aRect, bool aContained, int aAccuracy ) const
{
bool is_out_of_box = false;
EDA_RECT arect = aRect;
arect.Inflate( aAccuracy );
CRect rect = m_Poly->GetBoundingBox();
EDA_RECT bbox;
bbox.SetOrigin( rect.left, rect.bottom );
bbox.SetEnd( rect.right, rect.top );
if( aContained )
return arect.Contains( bbox );
else // Test for intersection between aRect and the polygon
// For a polygon, using its bounding box has no sense here
{
// Fast test: if aRect is outside the polygon bounding box,
// rectangles cannot intersect
if( ! bbox.Intersects( arect ) )
return false;
// aRect is inside the polygon bounding box,
// and can intersect the polygon: use a fine test.
// aRect intersects the polygon if at least one aRect corner
// is inside the polygon
wxPoint corner = arect.GetOrigin();
if( HitTestInsideZone( corner ) )
return true;
corner.x = arect.GetEnd().x;
if( HitTestInsideZone( corner ) )
return true;
CRect rect = m_Poly->GetCornerBounds();
corner = arect.GetEnd();
if( rect.left < aRect.GetX() )
is_out_of_box = true;
if( HitTestInsideZone( corner ) )
return true;
if( rect.top < aRect.GetY() )
is_out_of_box = true;
corner.x = arect.GetOrigin().x;
if( rect.right > aRect.GetRight() )
is_out_of_box = true;
if( HitTestInsideZone( corner ) )
return true;
if( rect.bottom > aRect.GetBottom() )
is_out_of_box = true;
// No corner inside arect, but outlines can intersect arect
// if one of outline corners is inside arect
int count = m_Poly->GetCornersCount();
for( int ii =0; ii < count; ii++ )
{
if( arect.Contains( m_Poly->GetPos( ii ) ) )
return true;
}
return is_out_of_box ? false : true;
return false;
}
}
......
......@@ -151,7 +151,7 @@ public:
void DrawWhileCreateOutline( EDA_DRAW_PANEL* panel, wxDC* DC,
GR_DRAWMODE draw_mode = GR_OR );
/* Function GetBoundingBox
/** Function GetBoundingBox
* @return an EDA_RECT that is the bounding box of the zone outline
*/
EDA_RECT GetBoundingBox() const;
......@@ -253,8 +253,26 @@ public:
void SetOutline( CPolyLine* aOutline ) { m_Poly = aOutline; }
/**
* Function HitTest
* tests if a point is near an outline edge or a corner of this zone.
* @param aRefPos A wxPoint to test
* @return bool - true if a hit, else false
*/
virtual bool HitTest( const wxPoint& aPosition );
/**
* Function HitTest
* tests if a point is inside the zone area, i.e. inside the main outline
* and outside holes.
* @param aRefPos A wxPoint to test
* @return bool - true if a hit, else false
*/
bool HitTestInsideZone( const wxPoint& aPosition ) const
{
return m_Poly->TestPointInside( aPosition.x, aPosition.y );
}
/**
* Function HitTestFilledArea
* tests if the given wxPoint is within the bounds of a filled area of this zone.
......@@ -360,7 +378,10 @@ public:
*/
bool HitTestForEdge( const wxPoint& refPos );
virtual bool HitTest( const EDA_RECT& aRect ) const;
/** @copydoc BOARD_ITEM::HitTest(const EDA_RECT& aRect,
* bool aContained = true, int aAccuracy ) const
*/
bool HitTest( const EDA_RECT& aRect, bool aContained = true, int aAccuracy = 0 ) const;
/**
* Function Fill_Zone
......
......@@ -102,7 +102,7 @@ bool BOARD::CombineAllAreasInNet( PICKED_ITEMS_LIST* aDeletedList, int aNetCode,
continue;
// legal polygon
CRect b1 = curr_area->Outline()->GetCornerBounds();
CRect b1 = curr_area->Outline()->GetBoundingBox();
bool mod_ia1 = false;
for( unsigned ia2 = m_ZoneDescriptorList.size() - 1; ia2 > ia1; ia2-- )
......@@ -121,7 +121,7 @@ bool BOARD::CombineAllAreasInNet( PICKED_ITEMS_LIST* aDeletedList, int aNetCode,
if( curr_area->GetLayer() != area2->GetLayer() )
continue;
CRect b2 = area2->Outline()->GetCornerBounds();
CRect b2 = area2->Outline()->GetBoundingBox();
if( !( b1.left > b2.right || b1.right < b2.left
|| b1.bottom > b2.top || b1.top < b2.bottom ) )
......@@ -194,8 +194,8 @@ bool BOARD::TestAreaIntersection( ZONE_CONTAINER* area_ref, ZONE_CONTAINER* area
CPolyLine* poly2 = area_to_test->Outline();
// test bounding rects
CRect b1 = poly1->GetCornerBounds();
CRect b2 = poly2->GetCornerBounds();
CRect b1 = poly1->GetBoundingBox();
CRect b2 = poly2->GetBoundingBox();
if( b1.bottom > b2.top || b1.top < b2.bottom ||
b1.left > b2.right || b1.right < b2.left )
......
......@@ -582,14 +582,7 @@ int CPolyLine::GetEndContour( int ic )
}
CRect CPolyLine::GetBounds()
{
CRect r = GetCornerBounds();
return r;
}
CRect CPolyLine::GetCornerBounds()
CRect CPolyLine::GetBoundingBox()
{
CRect r;
......@@ -608,7 +601,7 @@ CRect CPolyLine::GetCornerBounds()
}
CRect CPolyLine::GetCornerBounds( int icont )
CRect CPolyLine::GetBoundingBox( int icont )
{
CRect r;
......@@ -1381,7 +1374,7 @@ bool CPolyLine::IsPolygonSelfIntersecting()
cr.reserve( n_cont );
for( int icont = 0; icont<n_cont; icont++ )
cr.push_back( GetCornerBounds( icont ) );
cr.push_back( GetBoundingBox( icont ) );
for( int icont = 0; icont<n_cont; icont++ )
{
......
......@@ -296,9 +296,8 @@ public:
void MoveOrigin( int x_off, int y_off );
// misc. functions
CRect GetBounds();
CRect GetCornerBounds();
CRect GetCornerBounds( int icont );
CRect GetBoundingBox();
CRect GetBoundingBox( int icont );
void Copy( const CPolyLine* src );
bool TestPointInside( int x, int y );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment