2015-02-18 00:29:54 +00:00
|
|
|
/*
|
|
|
|
* KiRouter - a push-and-(sometimes-)shove PCB router
|
|
|
|
*
|
|
|
|
* Copyright (C) 2013-2015 CERN
|
|
|
|
* Author: Tomasz Wlostowski <tomasz.wlostowski@cern.ch>
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify it
|
|
|
|
* under the terms of the GNU General Public License as published by the
|
|
|
|
* Free Software Foundation, either version 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 <boost/foreach.hpp>
|
|
|
|
#include <boost/optional.hpp>
|
|
|
|
|
|
|
|
#include <colors.h>
|
|
|
|
#include <class_board.h>
|
|
|
|
#include <class_board_item.h>
|
|
|
|
#include <class_netinfo.h>
|
|
|
|
|
|
|
|
#include "trace.h"
|
|
|
|
|
|
|
|
#include "pns_node.h"
|
|
|
|
#include "pns_walkaround.h"
|
|
|
|
#include "pns_shove.h"
|
|
|
|
#include "pns_utils.h"
|
|
|
|
#include "pns_router.h"
|
|
|
|
#include "pns_diff_pair_placer.h"
|
|
|
|
#include "pns_solid.h"
|
|
|
|
#include "pns_topology.h"
|
|
|
|
|
|
|
|
using boost::optional;
|
|
|
|
|
|
|
|
PNS_DIFF_PAIR_PLACER::PNS_DIFF_PAIR_PLACER( PNS_ROUTER* aRouter ) :
|
|
|
|
PNS_PLACEMENT_ALGO ( aRouter )
|
|
|
|
{
|
2015-03-10 14:38:27 +00:00
|
|
|
m_state = RT_START;
|
|
|
|
m_chainedPlacement = false;
|
2015-02-18 00:29:54 +00:00
|
|
|
m_initialDiagonal = false;
|
|
|
|
m_startDiagonal = false;
|
2015-03-10 14:38:27 +00:00
|
|
|
m_fitOk = false;
|
|
|
|
m_netP = 0;
|
|
|
|
m_netN = 0;
|
|
|
|
m_iteration = 0;
|
2015-02-18 00:29:54 +00:00
|
|
|
m_world = NULL;
|
|
|
|
m_shove = NULL;
|
|
|
|
m_currentNode = NULL;
|
2015-03-10 14:38:27 +00:00
|
|
|
m_lastNode = NULL;
|
|
|
|
m_placingVia = false;
|
|
|
|
m_viaDiameter = 0;
|
|
|
|
m_viaDrill = 0;
|
|
|
|
m_currentWidth = 0;
|
|
|
|
m_currentNet = 0;
|
|
|
|
m_currentLayer = 0;
|
|
|
|
m_startsOnVia = false;
|
|
|
|
m_orthoMode = false;
|
|
|
|
m_snapOnTarget = false;
|
|
|
|
m_currentEndItem = NULL;
|
|
|
|
m_currentMode = RM_MarkObstacles;
|
2015-02-18 00:29:54 +00:00
|
|
|
m_idle = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
PNS_DIFF_PAIR_PLACER::~PNS_DIFF_PAIR_PLACER()
|
|
|
|
{
|
|
|
|
if( m_shove )
|
|
|
|
delete m_shove;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
void PNS_DIFF_PAIR_PLACER::setWorld( PNS_NODE* aWorld )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
m_world = aWorld;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
const PNS_VIA PNS_DIFF_PAIR_PLACER::makeVia( const VECTOR2I& aP, int aNet )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
const PNS_LAYERSET layers( m_sizes.GetLayerTop(), m_sizes.GetLayerBottom() );
|
|
|
|
|
|
|
|
PNS_VIA v( aP, layers, m_sizes.ViaDiameter(), m_sizes.ViaDrill(), -1, m_sizes.ViaType() );
|
2015-02-18 16:53:46 +00:00
|
|
|
v.SetNet( aNet );
|
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
return v;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
void PNS_DIFF_PAIR_PLACER::SetOrthoMode ( bool aOrthoMode )
|
|
|
|
{
|
|
|
|
m_orthoMode = aOrthoMode;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( !m_idle )
|
|
|
|
Move( m_currentEnd, NULL );
|
2015-02-18 00:29:54 +00:00
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
bool PNS_DIFF_PAIR_PLACER::ToggleVia( bool aEnabled )
|
|
|
|
{
|
|
|
|
m_placingVia = aEnabled;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( !m_idle )
|
|
|
|
Move( m_currentEnd, NULL );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
bool PNS_DIFF_PAIR_PLACER::rhMarkObstacles( const VECTOR2I& aP )
|
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !routeHead( aP ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
bool collP = m_currentNode->CheckColliding( &m_currentTrace.PLine() );
|
|
|
|
bool collN = m_currentNode->CheckColliding( &m_currentTrace.NLine() );
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
m_fitOk = !( collP || collN ) ;
|
2015-02-18 00:29:54 +00:00
|
|
|
return m_fitOk;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
bool PNS_DIFF_PAIR_PLACER::propagateDpHeadForces ( const VECTOR2I& aP, VECTOR2I& aNewP )
|
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
PNS_VIA virtHead = makeVia( aP, -1 );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( m_placingVia )
|
|
|
|
virtHead.SetDiameter( viaGap() + 2 * virtHead.Diameter() );
|
|
|
|
else
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
virtHead.SetLayer( m_currentLayer );
|
|
|
|
virtHead.SetDiameter( m_sizes.DiffPairGap() + 2 * m_sizes.TrackWidth() );
|
2015-02-18 00:29:54 +00:00
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
VECTOR2I lead( 0, 0 );// = aP - m_currentStart ;
|
2015-02-18 00:29:54 +00:00
|
|
|
VECTOR2I force;
|
2015-02-18 16:53:46 +00:00
|
|
|
bool solidsOnly = true;
|
2015-02-18 00:29:54 +00:00
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( m_currentMode == RM_MarkObstacles )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
aNewP = aP;
|
2015-02-18 16:53:46 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else if( m_currentMode == RM_Walkaround )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
solidsOnly = false;
|
|
|
|
}
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
// fixme: I'm too lazy to do it well. Circular approximaton will do for the moment.
|
|
|
|
if( virtHead.PushoutForce( m_currentNode, lead, force, solidsOnly, 40 ) )
|
|
|
|
{
|
|
|
|
aNewP = aP + force;
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
bool PNS_DIFF_PAIR_PLACER::attemptWalk ( PNS_NODE* aNode, PNS_DIFF_PAIR* aCurrent, PNS_DIFF_PAIR& aWalk, bool aPFirst, bool aWindCw, bool aSolidsOnly )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
PNS_WALKAROUND walkaround( aNode, Router() );
|
|
|
|
PNS_WALKAROUND::WALKAROUND_STATUS wf1;
|
|
|
|
|
2015-02-26 15:34:10 +00:00
|
|
|
Router()->GetClearanceFunc()->OverrideClearance( true, aCurrent->NetP(), aCurrent->NetN(), aCurrent->Gap() );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
walkaround.SetSolidsOnly( aSolidsOnly );
|
|
|
|
walkaround.SetIterationLimit( Settings().WalkaroundIterationLimit() );
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
PNS_SHOVE shove( aNode, Router() );
|
2015-02-18 00:29:54 +00:00
|
|
|
PNS_LINE walkP, walkN;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
aWalk = *aCurrent;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
int iter = 0;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
PNS_DIFF_PAIR cur( *aCurrent );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
bool currentIsP = aPFirst;
|
|
|
|
|
|
|
|
int mask = aSolidsOnly ? PNS_ITEM::SOLID : PNS_ITEM::ANY;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
do
|
|
|
|
{
|
|
|
|
PNS_LINE preWalk = ( currentIsP ? cur.PLine() : cur.NLine() );
|
|
|
|
PNS_LINE preShove = ( currentIsP ? cur.NLine() : cur.PLine() );
|
2015-02-18 00:29:54 +00:00
|
|
|
PNS_LINE postWalk;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !aNode->CheckColliding ( &preWalk, mask ) )
|
|
|
|
{
|
2015-02-18 00:29:54 +00:00
|
|
|
currentIsP = !currentIsP;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( !aNode->CheckColliding( &preShove, mask ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
break;
|
|
|
|
else
|
|
|
|
continue;
|
|
|
|
}
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
wf1 = walkaround.Route( preWalk, postWalk, false );
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( wf1 != PNS_WALKAROUND::DONE )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
PNS_LINE postShove( preShove );
|
|
|
|
|
2015-03-02 16:21:04 +00:00
|
|
|
shove.ForceClearance( true, cur.Gap() - 2 * PNS_HULL_MARGIN );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
PNS_SHOVE::SHOVE_STATUS sh1;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
sh1 = shove.ProcessSingleLine( &postWalk, &preShove, &postShove );
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( sh1 != PNS_SHOVE::SH_OK )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
postWalk.Line().Simplify();
|
|
|
|
postShove.Line().Simplify();
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
cur.SetShape( postWalk.CLine(), postShove.CLine(), !currentIsP );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
currentIsP = !currentIsP;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !aNode->CheckColliding( &postShove, mask ) )
|
|
|
|
break;
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
iter++;
|
2015-02-18 16:53:46 +00:00
|
|
|
}
|
|
|
|
while( iter < 3 );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( iter == 3 )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
aWalk.SetShape( cur.CP(), cur.CN() );
|
|
|
|
Router()->GetClearanceFunc()->OverrideClearance( false );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
bool PNS_DIFF_PAIR_PLACER::tryWalkDp( PNS_NODE* aNode, PNS_DIFF_PAIR &aPair, bool aSolidsOnly )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
PNS_DIFF_PAIR best;
|
|
|
|
double bestScore = 100000000000000.0;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
for( int attempt = 0; attempt <= 1; attempt++ )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
PNS_DIFF_PAIR p;
|
|
|
|
PNS_NODE *tmp = m_currentNode->Branch();
|
|
|
|
|
|
|
|
bool pfirst = attempt % 2 ? true : false;
|
|
|
|
bool wind_cw = attempt / 2 ? true : false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( attemptWalk ( tmp, &aPair, p, pfirst, wind_cw, aSolidsOnly ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
// double len = p.TotalLength();
|
|
|
|
double cl = p.CoupledLength();
|
|
|
|
double skew = p.Skew();
|
|
|
|
|
|
|
|
double score = cl + fabs(skew) * 3.0;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( score < bestScore )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
bestScore = score;
|
|
|
|
best = p;
|
|
|
|
}
|
|
|
|
}
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
delete tmp;
|
|
|
|
}
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( bestScore > 0.0 )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
PNS_OPTIMIZER optimizer( m_currentNode );
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
aPair.SetShape( best );
|
|
|
|
optimizer.Optimize( &aPair );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
bool PNS_DIFF_PAIR_PLACER::rhWalkOnly( const VECTOR2I& aP )
|
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !routeHead ( aP ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
m_fitOk = tryWalkDp( m_currentNode, m_currentTrace, false );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
return m_fitOk;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
bool PNS_DIFF_PAIR_PLACER::route( const VECTOR2I& aP )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
switch( m_currentMode )
|
|
|
|
{
|
|
|
|
case RM_MarkObstacles:
|
|
|
|
return rhMarkObstacles( aP );
|
|
|
|
case RM_Walkaround:
|
|
|
|
return rhWalkOnly ( aP );
|
|
|
|
case RM_Shove:
|
|
|
|
return rhShoveOnly ( aP );
|
|
|
|
default:
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
bool PNS_DIFF_PAIR_PLACER::rhShoveOnly( const VECTOR2I& aP )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
m_currentNode = m_shove->CurrentNode();
|
|
|
|
|
|
|
|
bool ok = routeHead ( aP );
|
|
|
|
|
|
|
|
m_fitOk = false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !ok )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !tryWalkDp( m_currentNode, m_currentTrace, true ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
PNS_LINE pLine( m_currentTrace.PLine() );
|
|
|
|
PNS_LINE nLine( m_currentTrace.NLine() );
|
2015-02-18 00:29:54 +00:00
|
|
|
PNS_ITEMSET head;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
head.Add( &pLine );
|
|
|
|
head.Add( &nLine );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
PNS_SHOVE::SHOVE_STATUS status = m_shove->ShoveMultiLines( head );
|
|
|
|
|
|
|
|
m_currentNode = m_shove->CurrentNode();
|
|
|
|
|
|
|
|
if( status == PNS_SHOVE::SH_OK )
|
|
|
|
{
|
|
|
|
m_currentNode = m_shove->CurrentNode();
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( !m_currentNode->CheckColliding( &m_currentTrace.PLine() ) &&
|
|
|
|
!m_currentNode->CheckColliding( &m_currentTrace.NLine() ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
m_fitOk = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return m_fitOk;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const PNS_ITEMSET PNS_DIFF_PAIR_PLACER::Traces()
|
|
|
|
{
|
|
|
|
PNS_ITEMSET t;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
t.Add( const_cast<PNS_LINE*>( &m_currentTrace.PLine() ) );
|
|
|
|
t.Add( const_cast<PNS_LINE*>( &m_currentTrace.NLine() ) );
|
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
return t;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PNS_DIFF_PAIR_PLACER::FlipPosture()
|
|
|
|
{
|
|
|
|
m_startDiagonal = !m_startDiagonal;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !m_idle )
|
|
|
|
Move( m_currentEnd, NULL );
|
2015-02-18 00:29:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
PNS_NODE* PNS_DIFF_PAIR_PLACER::CurrentNode( bool aLoopsRemoved ) const
|
|
|
|
{
|
|
|
|
if( m_lastNode )
|
|
|
|
return m_lastNode;
|
|
|
|
|
|
|
|
return m_currentNode;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool PNS_DIFF_PAIR_PLACER::SetLayer( int aLayer )
|
|
|
|
{
|
|
|
|
if( m_idle )
|
|
|
|
{
|
|
|
|
m_currentLayer = aLayer;
|
|
|
|
return true;
|
2015-02-18 16:53:46 +00:00
|
|
|
} else if( m_chainedPlacement )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
2015-02-18 16:53:46 +00:00
|
|
|
else if( !m_prevPair )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
2015-02-18 16:53:46 +00:00
|
|
|
else if( m_prevPair->PrimP() || ( m_prevPair->PrimP()->OfKind( PNS_ITEM::VIA ) &&
|
|
|
|
m_prevPair->PrimP()->Layers().Overlaps( aLayer ) ) )
|
|
|
|
{
|
2015-02-18 00:29:54 +00:00
|
|
|
m_currentLayer = aLayer;
|
|
|
|
m_start = *m_prevPair;
|
2015-02-18 16:53:46 +00:00
|
|
|
initPlacement( false );
|
|
|
|
Move( m_currentEnd, NULL );
|
2015-02-18 00:29:54 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
int PNS_DIFF_PAIR_PLACER::matchDpSuffix( wxString aNetName, wxString& aComplementNet, wxString& aBaseDpName )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
int rv = 0;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( aNetName.EndsWith( "+" ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
aComplementNet = "-";
|
|
|
|
rv = 1;
|
2015-02-18 16:53:46 +00:00
|
|
|
}
|
|
|
|
else if( aNetName.EndsWith( "_P" ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
aComplementNet = "_N";
|
|
|
|
rv = 1;
|
2015-02-18 16:53:46 +00:00
|
|
|
}
|
|
|
|
else if( aNetName.EndsWith( "-" ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
aComplementNet = "+";
|
|
|
|
rv = -1;
|
2015-02-18 16:53:46 +00:00
|
|
|
}
|
|
|
|
else if( aNetName.EndsWith( "_N" ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
aComplementNet = "_P";
|
|
|
|
rv = -1;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( rv != 0 )
|
|
|
|
{
|
|
|
|
aBaseDpName = aNetName.Left( aNetName.Length() - aComplementNet.Length() );
|
2015-02-18 00:29:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return rv;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
OPT_VECTOR2I PNS_DIFF_PAIR_PLACER::getDanglingAnchor( PNS_NODE* aNode, PNS_ITEM* aItem )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
switch( aItem->Kind() )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
case PNS_ITEM::VIA:
|
|
|
|
case PNS_ITEM::SOLID:
|
2015-02-18 16:53:46 +00:00
|
|
|
return aItem->Anchor( 0 );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
case PNS_ITEM::SEGMENT:
|
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
PNS_SEGMENT* s =static_cast<PNS_SEGMENT*>( aItem );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
PNS_JOINT* jA = aNode->FindJoint( s->Seg().A, s );
|
|
|
|
PNS_JOINT* jB = aNode->FindJoint( s->Seg().B, s );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( jA->LinkCount() == 1 )
|
2015-02-18 00:29:54 +00:00
|
|
|
return s->Seg().A;
|
2015-02-18 16:53:46 +00:00
|
|
|
else if( jB->LinkCount() == 1 )
|
2015-02-18 00:29:54 +00:00
|
|
|
return s->Seg().B;
|
2015-02-18 16:53:46 +00:00
|
|
|
else
|
2015-02-18 00:29:54 +00:00
|
|
|
return OPT_VECTOR2I();
|
|
|
|
}
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
default:
|
|
|
|
return OPT_VECTOR2I();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
bool PNS_DIFF_PAIR_PLACER::findDpPrimitivePair( const VECTOR2I& aP, PNS_ITEM* aItem, PNS_DP_PRIMITIVE_PAIR& aPair )
|
|
|
|
{
|
|
|
|
if( !aItem || !aItem->Parent() || !aItem->Parent()->GetNet() )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
wxString netNameP = aItem->Parent()->GetNet()->GetNetname();
|
|
|
|
wxString netNameN, netNameBase;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
BOARD* brd = Router()->GetBoard();
|
2015-02-18 00:29:54 +00:00
|
|
|
PNS_ITEM *primRef = NULL, *primP = NULL, *primN = NULL;
|
|
|
|
|
|
|
|
int refNet;
|
|
|
|
|
|
|
|
wxString suffix;
|
|
|
|
|
|
|
|
int r = matchDpSuffix ( netNameP, suffix, netNameBase );
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( r == 0 )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
2015-02-18 16:53:46 +00:00
|
|
|
else if( r == 1 )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
primRef = primP = static_cast<PNS_SOLID*>( aItem );
|
2015-02-18 00:29:54 +00:00
|
|
|
netNameN = netNameBase + suffix;
|
2015-02-18 16:53:46 +00:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
primRef = primN = static_cast<PNS_SOLID*>( aItem );
|
2015-02-18 00:29:54 +00:00
|
|
|
netNameN = netNameP;
|
|
|
|
netNameP = netNameBase + suffix;
|
|
|
|
}
|
|
|
|
|
2015-03-05 09:01:54 +00:00
|
|
|
NETINFO_ITEM* netInfoP = brd->FindNet( netNameP );
|
|
|
|
NETINFO_ITEM* netInfoN = brd->FindNet( netNameN );
|
|
|
|
|
|
|
|
if( !netInfoP || !netInfoN )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
int netP = netInfoP->GetNet();
|
|
|
|
int netN = netInfoN->GetNet();
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( primP )
|
2015-02-18 00:29:54 +00:00
|
|
|
refNet = netN;
|
|
|
|
else
|
|
|
|
refNet = netP;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
std::set<PNS_ITEM*> items;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
OPT_VECTOR2I refAnchor = getDanglingAnchor( m_currentNode, primRef );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !refAnchor )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
m_currentNode->AllItemsInNet( refNet, items );
|
|
|
|
double bestDist = std::numeric_limits<double>::max();
|
|
|
|
bool found = false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
BOOST_FOREACH(PNS_ITEM* item, items )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
if( item->Kind() == aItem->Kind() )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
OPT_VECTOR2I anchor = getDanglingAnchor( m_currentNode, item );
|
|
|
|
if( !anchor )
|
2015-02-18 00:29:54 +00:00
|
|
|
continue;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
double dist = ( *anchor - *refAnchor ).EuclideanNorm();
|
|
|
|
|
|
|
|
if( dist < bestDist )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
found = true;
|
|
|
|
bestDist = dist;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( refNet == netP )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
aPair = PNS_DP_PRIMITIVE_PAIR ( item, primRef );
|
2015-02-18 16:53:46 +00:00
|
|
|
aPair.SetAnchors( *anchor, *refAnchor );
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
aPair = PNS_DP_PRIMITIVE_PAIR( primRef, item );
|
|
|
|
aPair.SetAnchors( *refAnchor, *anchor );
|
2015-02-18 00:29:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return found;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
int PNS_DIFF_PAIR_PLACER::viaGap() const
|
|
|
|
{
|
|
|
|
return m_sizes.DiffPairViaGap();
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
int PNS_DIFF_PAIR_PLACER::gap() const
|
|
|
|
{
|
|
|
|
return m_sizes.DiffPairGap() + m_sizes.DiffPairWidth();
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
bool PNS_DIFF_PAIR_PLACER::Start( const VECTOR2I& aP, PNS_ITEM* aStartItem )
|
|
|
|
{
|
|
|
|
VECTOR2I p( aP );
|
|
|
|
|
|
|
|
bool split;
|
|
|
|
|
|
|
|
if( Router()->SnappingEnabled() )
|
|
|
|
p = Router()->SnapToItem( aStartItem, aP, split );
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !aStartItem )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
Router()->SetFailureReason( _( "Can't start a differential pair "
|
|
|
|
" in the middle of nowhere." ) );
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
PNS_DP_PRIMITIVE_PAIR start;
|
|
|
|
|
|
|
|
m_currentNode = Router()->GetWorld();
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !findDpPrimitivePair( aP, aStartItem, m_start ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
Router()->SetFailureReason( _( "Unable to find complementary differential pair "
|
|
|
|
"net. Make sure the names of the nets belonging "
|
|
|
|
"to a differential pair end with either _N/_P or +/-." ) );
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_netP = m_start.PrimP()->Net();
|
|
|
|
m_netN = m_start.PrimN()->Net();
|
|
|
|
|
|
|
|
m_currentStart = p;
|
|
|
|
m_currentEnd = p;
|
|
|
|
m_placingVia = false;
|
|
|
|
m_chainedPlacement = false;
|
|
|
|
|
|
|
|
initPlacement( false );
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
void PNS_DIFF_PAIR_PLACER::initPlacement( bool aSplitSeg )
|
|
|
|
{
|
|
|
|
m_idle = false;
|
|
|
|
m_orthoMode = false;
|
|
|
|
m_currentEndItem = NULL;
|
|
|
|
m_startDiagonal = m_initialDiagonal;
|
|
|
|
|
|
|
|
PNS_NODE* world = Router()->GetWorld();
|
|
|
|
|
|
|
|
world->KillChildren();
|
|
|
|
PNS_NODE* rootNode = world->Branch();
|
|
|
|
|
|
|
|
setWorld( rootNode );
|
|
|
|
|
|
|
|
m_lastNode = NULL;
|
|
|
|
m_currentNode = rootNode;
|
|
|
|
m_currentMode = Settings().Mode();
|
|
|
|
|
|
|
|
if( m_shove )
|
|
|
|
delete m_shove;
|
|
|
|
|
|
|
|
m_shove = NULL;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
if( m_currentMode == RM_Shove || m_currentMode == RM_Smart )
|
|
|
|
{
|
|
|
|
m_shove = new PNS_SHOVE( m_currentNode, Router() );
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
bool PNS_DIFF_PAIR_PLACER::routeHead( const VECTOR2I& aP )
|
|
|
|
{
|
|
|
|
m_fitOk = false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
PNS_DP_GATEWAYS gwsEntry( gap() );
|
|
|
|
PNS_DP_GATEWAYS gwsTarget( gap() );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !m_prevPair )
|
2015-02-18 00:29:54 +00:00
|
|
|
m_prevPair = m_start;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
gwsEntry.BuildFromPrimitivePair( *m_prevPair, m_startDiagonal );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
PNS_DP_PRIMITIVE_PAIR target;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( findDpPrimitivePair ( aP, m_currentEndItem, target ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
gwsTarget.BuildFromPrimitivePair( target, m_startDiagonal );
|
|
|
|
m_snapOnTarget = true;
|
|
|
|
} else {
|
|
|
|
VECTOR2I fp;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !propagateDpHeadForces( aP, fp ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
gwsTarget.SetFitVias( m_placingVia, m_sizes.ViaDiameter(), viaGap() );
|
|
|
|
gwsTarget.BuildForCursor( fp );
|
|
|
|
gwsTarget.BuildOrthoProjections( gwsEntry, fp, m_orthoMode ? 200 : -200 );
|
2015-02-18 00:29:54 +00:00
|
|
|
m_snapOnTarget = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
m_currentTrace = PNS_DIFF_PAIR();
|
2015-02-18 16:53:46 +00:00
|
|
|
m_currentTrace.SetGap( gap() );
|
2015-02-18 00:29:54 +00:00
|
|
|
m_currentTrace.SetLayer( m_currentLayer );
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if ( gwsEntry.FitGateways( gwsEntry, gwsTarget, m_startDiagonal, m_currentTrace ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
m_currentTrace.SetNets( m_netP, m_netN );
|
|
|
|
m_currentTrace.SetWidth( m_sizes.DiffPairWidth() );
|
|
|
|
m_currentTrace.SetGap( m_sizes.DiffPairGap() );
|
|
|
|
|
|
|
|
if( m_placingVia )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
m_currentTrace.AppendVias ( makeVia ( m_currentTrace.CP().CPoint(-1), m_netP ),
|
2015-02-18 00:29:54 +00:00
|
|
|
makeVia ( m_currentTrace.CN().CPoint(-1), m_netN ) );
|
|
|
|
}
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
bool PNS_DIFF_PAIR_PLACER::Move( const VECTOR2I& aP , PNS_ITEM* aEndItem )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
m_currentEndItem = aEndItem;
|
|
|
|
m_fitOk = false;
|
|
|
|
|
|
|
|
delete m_lastNode;
|
|
|
|
m_lastNode = NULL;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !route( aP ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
|
|
|
PNS_NODE* latestNode = m_currentNode;
|
|
|
|
m_lastNode = latestNode->Branch();
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
assert( m_lastNode != NULL );
|
2015-02-18 00:29:54 +00:00
|
|
|
m_currentEnd = aP;
|
|
|
|
|
|
|
|
updateLeadingRatLine();
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
void PNS_DIFF_PAIR_PLACER::UpdateSizes( const PNS_SIZES_SETTINGS& aSizes )
|
|
|
|
{
|
|
|
|
m_sizes = aSizes;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
if( !m_idle )
|
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
initPlacement();
|
|
|
|
Move( m_currentEnd, NULL );
|
2015-02-18 00:29:54 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
bool PNS_DIFF_PAIR_PLACER::FixRoute( const VECTOR2I& aP, PNS_ITEM* aEndItem )
|
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
if( !m_fitOk )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( m_currentTrace.CP().SegmentCount() < 1 ||
|
|
|
|
m_currentTrace.CN().SegmentCount() < 1 )
|
2015-02-18 00:29:54 +00:00
|
|
|
return false;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( m_currentTrace.CP().SegmentCount() > 1 )
|
|
|
|
m_initialDiagonal = !DIRECTION_45( m_currentTrace.CP().CSegment( -2 ) ).IsDiagonal();
|
|
|
|
|
|
|
|
PNS_TOPOLOGY topo( m_lastNode );
|
|
|
|
|
|
|
|
if( !m_snapOnTarget && !m_currentTrace.EndsWithVias() )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
SHAPE_LINE_CHAIN newP ( m_currentTrace.CP() );
|
|
|
|
SHAPE_LINE_CHAIN newN ( m_currentTrace.CN() );
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( newP.SegmentCount() > 1 && newN.SegmentCount() > 1 )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
2015-02-18 16:53:46 +00:00
|
|
|
newP.Remove( -1, -1 );
|
|
|
|
newN.Remove( -1, -1 );
|
2015-02-18 00:29:54 +00:00
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
m_currentTrace.SetShape( newP, newN );
|
2015-02-18 00:29:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if( m_currentTrace.EndsWithVias() )
|
|
|
|
{
|
|
|
|
m_lastNode->Add( m_currentTrace.PLine().Via().Clone() );
|
|
|
|
m_lastNode->Add( m_currentTrace.NLine().Via().Clone() );
|
|
|
|
m_chainedPlacement = false;
|
|
|
|
} else
|
|
|
|
m_chainedPlacement = !m_snapOnTarget;
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
PNS_LINE lineP( m_currentTrace.PLine() );
|
|
|
|
PNS_LINE lineN( m_currentTrace.NLine() );
|
|
|
|
|
|
|
|
m_lastNode->Add( &lineP );
|
|
|
|
m_lastNode->Add( &lineN );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
|
|
|
topo.SimplifyLine( &lineP );
|
|
|
|
topo.SimplifyLine( &lineN );
|
|
|
|
|
|
|
|
m_prevPair = m_currentTrace.EndingPrimitives();
|
|
|
|
|
|
|
|
Router()->CommitRouting( m_lastNode );
|
|
|
|
|
|
|
|
m_lastNode = NULL;
|
|
|
|
m_placingVia = false;
|
2015-02-18 16:53:46 +00:00
|
|
|
|
|
|
|
if( m_snapOnTarget )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
m_idle = true;
|
2015-02-18 16:53:46 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2015-02-18 00:29:54 +00:00
|
|
|
initPlacement();
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void PNS_DIFF_PAIR_PLACER::GetModifiedNets( std::vector<int> &aNets ) const
|
|
|
|
{
|
|
|
|
aNets.push_back( m_netP );
|
|
|
|
aNets.push_back( m_netN );
|
|
|
|
}
|
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
|
2015-02-18 00:29:54 +00:00
|
|
|
void PNS_DIFF_PAIR_PLACER::updateLeadingRatLine()
|
|
|
|
{
|
|
|
|
SHAPE_LINE_CHAIN ratLineN, ratLineP;
|
2015-02-18 16:53:46 +00:00
|
|
|
PNS_TOPOLOGY topo( m_lastNode );
|
2015-02-18 00:29:54 +00:00
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( topo.LeadingRatLine( &m_currentTrace.PLine(), ratLineP ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
Router()->DisplayDebugLine( ratLineP, 1, 10000 );
|
2015-02-18 16:53:46 +00:00
|
|
|
}
|
2015-02-18 00:29:54 +00:00
|
|
|
|
2015-02-18 16:53:46 +00:00
|
|
|
if( topo.LeadingRatLine ( &m_currentTrace.NLine(), ratLineN ) )
|
2015-02-18 00:29:54 +00:00
|
|
|
{
|
|
|
|
Router()->DisplayDebugLine( ratLineN, 3, 10000 );
|
2015-02-18 16:53:46 +00:00
|
|
|
}
|
2015-02-18 00:29:54 +00:00
|
|
|
}
|