You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

737 lines
22 KiB

  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2004-2017 Jean-Pierre Charras, jp.charras at wanadoo.fr
  5. * Copyright (C) 2011 Wayne Stambaugh <stambaughw@verizon.net>
  6. * Copyright (C) 1992-2017 KiCad Developers, see AUTHORS.txt for contributors.
  7. *
  8. * This program is free software; you can redistribute it and/or
  9. * modify it under the terms of the GNU General Public License
  10. * as published by the Free Software Foundation; either version 2
  11. * of the License, or (at your option) any later version.
  12. *
  13. * This program is distributed in the hope that it will be useful,
  14. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. * GNU General Public License for more details.
  17. *
  18. * You should have received a copy of the GNU General Public License
  19. * along with this program; if not, you may find one here:
  20. * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
  21. * or you may search the http://www.gnu.org website for the version 2 license,
  22. * or you may write to the Free Software Foundation, Inc.,
  23. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
  24. */
  25. /**
  26. * @file clean.cpp
  27. * @brief functions to clean tracks: remove null length and redundant segments
  28. */
  29. #include <fctsys.h>
  30. #include <class_drawpanel.h>
  31. #include <wxPcbStruct.h>
  32. #include <pcbnew.h>
  33. #include <class_board.h>
  34. #include <class_track.h>
  35. #include <dialog_cleaning_options.h>
  36. #include <board_commit.h>
  37. #include <connectivity.h>
  38. #include <connectivity_algo.h>
  39. // Helper class used to clean tracks and vias
  40. class TRACKS_CLEANER
  41. {
  42. public:
  43. TRACKS_CLEANER( BOARD* aPcb, BOARD_COMMIT& aCommit );
  44. /**
  45. * the cleanup function.
  46. * return true if some item was modified
  47. * @param aCleanVias = true to remove superimposed vias
  48. * @param aRemoveMisConnected = true to remove segments connecting 2 different nets
  49. * @param aMergeSegments = true to merge collinear segmenst and remove 0 len segm
  50. * @param aDeleteUnconnected = true to remove dangling tracks
  51. * (short circuits)
  52. */
  53. bool CleanupBoard( bool aCleanVias, bool aRemoveMisConnected,
  54. bool aMergeSegments, bool aDeleteUnconnected );
  55. private:
  56. /* finds and remove all track segments which are connected to more than one net.
  57. * (short circuits)
  58. */
  59. bool removeBadTrackSegments();
  60. /**
  61. * Removes redundant vias like vias at same location
  62. * or on pad through
  63. */
  64. bool cleanupVias();
  65. /**
  66. * Removes all the following THT vias on the same position of the
  67. * specified one
  68. */
  69. void removeDuplicatesOfVia( const VIA *aVia, std::set<BOARD_ITEM *>& aToRemove );
  70. /**
  71. * Removes all the following duplicates tracks of the specified one
  72. */
  73. void removeDuplicatesOfTrack( const TRACK* aTrack, std::set<BOARD_ITEM*>& aToRemove );
  74. /**
  75. * Removes dangling tracks
  76. */
  77. bool deleteDanglingTracks();
  78. /// Delete null length track segments
  79. bool deleteNullSegments();
  80. /// Try to merge the segment to a following collinear one
  81. bool MergeCollinearTracks( TRACK* aSegment );
  82. /**
  83. * Merge collinear segments and remove duplicated and null len segments
  84. */
  85. bool cleanupSegments();
  86. /**
  87. * helper function
  88. * Rebuild list of tracks, and connected tracks
  89. * this info must be rebuilt when tracks are erased
  90. */
  91. void buildTrackConnectionInfo();
  92. /**
  93. * helper function
  94. * merge aTrackRef and aCandidate, when possible,
  95. * i.e. when they are colinear, same width, and obviously same layer
  96. */
  97. TRACK* mergeCollinearSegmentIfPossible( TRACK* aTrackRef,
  98. TRACK* aCandidate, ENDPOINT_T aEndType );
  99. const ZONE_CONTAINER* zoneForTrackEndpoint( const TRACK* aTrack,
  100. ENDPOINT_T aEndPoint );
  101. bool testTrackEndpointDangling( TRACK* aTrack, ENDPOINT_T aEndPoint );
  102. BOARD* m_brd;
  103. BOARD_COMMIT& m_commit;
  104. bool removeItems( std::set<BOARD_ITEM*>& aItems )
  105. {
  106. bool isModified = false;
  107. for( auto item : aItems )
  108. {
  109. isModified = true;
  110. m_brd->Remove( item );
  111. m_commit.Removed( item );
  112. }
  113. return isModified;
  114. }
  115. };
  116. /* Install the cleanup dialog frame to know what should be cleaned
  117. */
  118. void PCB_EDIT_FRAME::Clean_Pcb()
  119. {
  120. DIALOG_CLEANING_OPTIONS dlg( this );
  121. if( dlg.ShowModal() != wxID_OK )
  122. return;
  123. // Old model has to be refreshed, GAL normally does not keep updating it
  124. Compile_Ratsnest( NULL, false );
  125. wxBusyCursor( dummy );
  126. BOARD_COMMIT commit( this );
  127. TRACKS_CLEANER cleaner( GetBoard(), commit );
  128. bool modified = cleaner.CleanupBoard( dlg.m_deleteShortCircuits, dlg.m_cleanVias,
  129. dlg.m_mergeSegments, dlg.m_deleteUnconnectedSegm );
  130. if( modified )
  131. {
  132. // Clear undo and redo lists to avoid inconsistencies between lists
  133. SetCurItem( NULL );
  134. commit.Push( _( "Board cleanup" ) );
  135. }
  136. m_canvas->Refresh( true );
  137. }
  138. void TRACKS_CLEANER::buildTrackConnectionInfo()
  139. {
  140. auto connectivity = m_brd->GetConnectivity();
  141. connectivity->Build(m_brd);
  142. // clear flags and variables used in cleanup
  143. for( auto track : m_brd->Tracks() )
  144. {
  145. track->SetState( START_ON_PAD | END_ON_PAD | BUSY, false );
  146. }
  147. for( auto track : m_brd->Tracks() )
  148. {
  149. // Mark track if connected to pads
  150. for( auto pad : connectivity->GetConnectedPads( track ) )
  151. {
  152. if( pad->HitTest( track->GetStart() ) )
  153. {
  154. track->SetState( START_ON_PAD, true );
  155. }
  156. if( pad->HitTest( track->GetEnd() ) )
  157. {
  158. track->SetState( END_ON_PAD, true );
  159. }
  160. }
  161. }
  162. }
  163. /* Main cleaning function.
  164. * Delete
  165. * - Redundant points on tracks (merge aligned segments)
  166. * - vias on pad
  167. * - null length segments
  168. */
  169. bool TRACKS_CLEANER::CleanupBoard( bool aRemoveMisConnected,
  170. bool aCleanVias,
  171. bool aMergeSegments,
  172. bool aDeleteUnconnected )
  173. {
  174. bool modified = false;
  175. // delete redundant vias
  176. if( aCleanVias )
  177. modified |= cleanupVias();
  178. // Remove null segments and intermediate points on aligned segments
  179. // If not asked, remove null segments only if remove misconnected is asked
  180. if( aMergeSegments )
  181. modified |= cleanupSegments();
  182. else if( aRemoveMisConnected )
  183. modified |= deleteNullSegments();
  184. buildTrackConnectionInfo();
  185. if( aRemoveMisConnected )
  186. modified |= removeBadTrackSegments();
  187. // Delete dangling tracks
  188. if( aDeleteUnconnected )
  189. {
  190. buildTrackConnectionInfo();
  191. if( deleteDanglingTracks() )
  192. {
  193. modified = true;
  194. // Removed tracks can leave aligned segments
  195. // (when a T was formed by tracks and the "vertical" segment
  196. // is removed)
  197. if( aMergeSegments )
  198. cleanupSegments();
  199. }
  200. }
  201. return modified;
  202. }
  203. TRACKS_CLEANER::TRACKS_CLEANER( BOARD* aPcb, BOARD_COMMIT& aCommit )
  204. : m_brd( aPcb ), m_commit( aCommit )
  205. {
  206. }
  207. bool TRACKS_CLEANER::removeBadTrackSegments()
  208. {
  209. auto connectivity = m_brd->GetConnectivity();
  210. std::set<BOARD_ITEM *> toRemove;
  211. for( auto segment : m_brd->Tracks() )
  212. {
  213. segment->SetState( FLAG0, false );
  214. for( auto testedPad : connectivity->GetConnectedPads( segment ) )
  215. {
  216. if( segment->GetNetCode() != testedPad->GetNetCode() )
  217. toRemove.insert( segment );
  218. }
  219. for( auto testedTrack : connectivity->GetConnectedTracks( segment ) )
  220. {
  221. if( segment->GetNetCode() != testedTrack->GetNetCode() && !testedTrack->GetState( FLAG0 ) )
  222. toRemove.insert( segment );
  223. }
  224. }
  225. return removeItems( toRemove );
  226. }
  227. void TRACKS_CLEANER::removeDuplicatesOfVia( const VIA *aVia, std::set<BOARD_ITEM *>& aToRemove )
  228. {
  229. VIA* next_via;
  230. for( VIA* alt_via = GetFirstVia( aVia->Next() ); alt_via != NULL; alt_via = next_via )
  231. {
  232. next_via = GetFirstVia( alt_via->Next() );
  233. if( ( alt_via->GetViaType() == VIA_THROUGH ) &&
  234. ( alt_via->GetStart() == aVia->GetStart() ) )
  235. aToRemove.insert ( alt_via );
  236. }
  237. }
  238. bool TRACKS_CLEANER::cleanupVias()
  239. {
  240. std::set<BOARD_ITEM*> toRemove;
  241. for( VIA* via = GetFirstVia( m_brd->m_Track ); via != NULL;
  242. via = GetFirstVia( via->Next() ) )
  243. {
  244. if( via->GetFlags() & TRACK_LOCKED )
  245. continue;
  246. // Correct via m_End defects (if any), should never happen
  247. if( via->GetStart() != via->GetEnd() )
  248. {
  249. wxFAIL_MSG( "Malformed via with mismatching ends" );
  250. via->SetEnd( via->GetStart() );
  251. }
  252. /* Important: these cleanups only do thru hole vias, they don't
  253. * (yet) handle high density interconnects */
  254. if( via->GetViaType() == VIA_THROUGH )
  255. {
  256. removeDuplicatesOfVia( via, toRemove );
  257. /* To delete through Via on THT pads at same location
  258. * Examine the list of connected pads:
  259. * if one through pad is found, the via can be removed */
  260. const auto pads = m_brd->GetConnectivity()->GetConnectedPads( via );
  261. for( const auto pad : pads )
  262. {
  263. const LSET all_cu = LSET::AllCuMask();
  264. if( ( pad->GetLayerSet() & all_cu ) == all_cu )
  265. {
  266. // redundant: delete the via
  267. toRemove.insert( via );
  268. break;
  269. }
  270. }
  271. }
  272. }
  273. return removeItems( toRemove );
  274. }
  275. /** Utility: does the endpoint unconnected processed for one endpoint of one track
  276. * Returns true if the track must be deleted, false if not necessarily */
  277. bool TRACKS_CLEANER::testTrackEndpointDangling( TRACK* aTrack, ENDPOINT_T aEndPoint )
  278. {
  279. auto connectivity = m_brd->GetConnectivity();
  280. VECTOR2I endpoint ;
  281. if( aTrack->Type() == PCB_TRACE_T )
  282. endpoint = aTrack->GetEndPoint( aEndPoint );
  283. else
  284. endpoint = aTrack->GetStart( );
  285. //wxASSERT ( connectivity->GetConnectivityAlgo()->ItemEntry( aTrack ) != nullptr );
  286. wxASSERT ( connectivity->GetConnectivityAlgo()->ItemEntry( aTrack ).GetItems().size() != 0 );
  287. auto citem = connectivity->GetConnectivityAlgo()->ItemEntry( aTrack ).GetItems().front();
  288. if( !citem->Valid() )
  289. return false;
  290. auto anchors = citem->Anchors();
  291. for( auto anchor : anchors )
  292. {
  293. if( anchor->Pos() == endpoint && anchor->IsDangling() )
  294. return true;
  295. }
  296. return false;
  297. }
  298. /* Delete dangling tracks
  299. * Vias:
  300. * If a via is only connected to a dangling track, it also will be removed
  301. */
  302. bool TRACKS_CLEANER::deleteDanglingTracks()
  303. {
  304. bool item_erased = false;
  305. bool modified = false;
  306. do // Iterate when at least one track is deleted
  307. {
  308. item_erased = false;
  309. TRACK* next_track;
  310. for( TRACK *track = m_brd->m_Track; track != NULL; track = next_track )
  311. {
  312. next_track = track->Next();
  313. bool flag_erase = false; // Start without a good reason to erase it
  314. /* if a track endpoint is not connected to a pad, test if
  315. * the endpoint is connected to another track or to a zone.
  316. * For via test, an enhancement could be to test if
  317. * connected to 2 items on different layers. Currently
  318. * a via must be connected to 2 items, that can be on the
  319. * same layer */
  320. // Check if there is nothing attached on the start
  321. if( !( track->GetState( START_ON_PAD ) ) )
  322. flag_erase |= testTrackEndpointDangling( track, ENDPOINT_START );
  323. // If not sure about removal, then check if there is nothing attached on the end
  324. if( !flag_erase && !track->GetState( END_ON_PAD ) )
  325. flag_erase |= testTrackEndpointDangling( track, ENDPOINT_END );
  326. if( flag_erase )
  327. {
  328. m_brd->Remove( track );
  329. m_commit.Removed( track );
  330. /* keep iterating, because a track connected to the deleted track
  331. * now perhaps is not connected and should be deleted */
  332. item_erased = true;
  333. modified = true;
  334. }
  335. }
  336. } while( item_erased );
  337. return modified;
  338. }
  339. // Delete null length track segments
  340. bool TRACKS_CLEANER::deleteNullSegments()
  341. {
  342. std::set<BOARD_ITEM *> toRemove;
  343. for( auto segment : m_brd->Tracks() )
  344. {
  345. if( segment->IsNull() ) // Length segment = 0; delete it
  346. toRemove.insert( segment );
  347. }
  348. return removeItems( toRemove );
  349. }
  350. void TRACKS_CLEANER::removeDuplicatesOfTrack( const TRACK *aTrack, std::set<BOARD_ITEM*>& aToRemove )
  351. {
  352. for( auto other : m_brd->Tracks() )
  353. {
  354. // New netcode, break out (can't be there any other)
  355. if( aTrack->GetNetCode() != other->GetNetCode() )
  356. continue;
  357. if( aTrack == other )
  358. continue;
  359. // Must be of the same type, on the same layer and the endpoints
  360. // must be the same (maybe swapped)
  361. if( ( aTrack->Type() == other->Type() ) &&
  362. ( aTrack->GetLayer() == other->GetLayer() ) )
  363. {
  364. if( ( ( aTrack->GetStart() == other->GetStart() ) &&
  365. ( aTrack->GetEnd() == other->GetEnd() ) ) ||
  366. ( ( aTrack->GetStart() == other->GetEnd() ) &&
  367. ( aTrack->GetEnd() == other->GetStart() ) ) )
  368. {
  369. aToRemove.insert( other );
  370. }
  371. }
  372. }
  373. }
  374. bool TRACKS_CLEANER::MergeCollinearTracks( TRACK* aSegment )
  375. {
  376. bool merged_this = false;
  377. for( ENDPOINT_T endpoint = ENDPOINT_START; endpoint <= ENDPOINT_END;
  378. endpoint = ENDPOINT_T( endpoint + 1 ) )
  379. {
  380. // search for a possible segment connected to the current endpoint of the current one
  381. TRACK* other = aSegment->Next();
  382. if( other )
  383. {
  384. other = aSegment->GetTrack( other, NULL, endpoint, true, false );
  385. if( other )
  386. {
  387. // the two segments must have the same width and the other
  388. // cannot be a via
  389. if( ( aSegment->GetWidth() == other->GetWidth() ) &&
  390. ( other->Type() == PCB_TRACE_T ) )
  391. {
  392. // There can be only one segment connected
  393. other->SetState( BUSY, true );
  394. TRACK* yet_another = aSegment->GetTrack( m_brd->m_Track, NULL,
  395. endpoint, true, false );
  396. other->SetState( BUSY, false );
  397. if( !yet_another )
  398. {
  399. // Try to merge them
  400. TRACK* segDelete = mergeCollinearSegmentIfPossible( aSegment,
  401. other, endpoint );
  402. // Merge succesful, the other one has to go away
  403. if( segDelete )
  404. {
  405. m_brd->Remove( segDelete );
  406. m_commit.Removed( segDelete );
  407. merged_this = true;
  408. }
  409. }
  410. }
  411. }
  412. }
  413. }
  414. return merged_this;
  415. }
  416. // Delete null length segments, and intermediate points ..
  417. bool TRACKS_CLEANER::cleanupSegments()
  418. {
  419. bool modified = false;
  420. // Easy things first
  421. modified |= deleteNullSegments();
  422. buildTrackConnectionInfo();
  423. std::set<BOARD_ITEM*> toRemove;
  424. // Delete redundant segments, i.e. segments having the same end points and layers
  425. // (can happens when blocks are copied on themselve)
  426. for( auto segment : m_brd->Tracks() )
  427. removeDuplicatesOfTrack( segment, toRemove );
  428. modified |= removeItems( toRemove );
  429. modified = true;
  430. if( modified )
  431. buildTrackConnectionInfo();
  432. // merge collinear segments:
  433. TRACK* nextsegment;
  434. for( TRACK* segment = m_brd->m_Track; segment; segment = nextsegment )
  435. {
  436. nextsegment = segment->Next();
  437. if( segment->Type() == PCB_TRACE_T )
  438. {
  439. bool merged_this = MergeCollinearTracks( segment );
  440. if( merged_this ) // The current segment was modified, retry to merge it again
  441. {
  442. nextsegment = segment->Next();
  443. modified = true;
  444. }
  445. }
  446. }
  447. return modified;
  448. }
  449. /* Utility: check for parallelism between two segments */
  450. static bool parallelismTest( int dx1, int dy1, int dx2, int dy2 )
  451. {
  452. /* The following condition list is ugly and repetitive, but I have
  453. * not a better way to express clearly the trivial cases. Hope the
  454. * compiler optimize it better than always doing the product
  455. * below... */
  456. // test for vertical alignment (easy to handle)
  457. if( dx1 == 0 )
  458. return dx2 == 0;
  459. if( dx2 == 0 )
  460. return dx1 == 0;
  461. // test for horizontal alignment (easy to handle)
  462. if( dy1 == 0 )
  463. return dy2 == 0;
  464. if( dy2 == 0 )
  465. return dy1 == 0;
  466. /* test for alignment in other cases: Do the usual cross product test
  467. * (the same as testing the slope, but without a division) */
  468. return ((double)dy1 * dx2 == (double)dx1 * dy2);
  469. }
  470. /** Function used by cleanupSegments.
  471. * Test if aTrackRef and aCandidate (which must have a common end) are collinear.
  472. * and see if the common point is not on a pad (i.e. if this common point can be removed).
  473. * the ending point of aTrackRef is the start point (aEndType == START)
  474. * or the end point (aEndType != START)
  475. * flags START_ON_PAD and END_ON_PAD must be set before calling this function
  476. * if the common point can be deleted, this function
  477. * change the common point coordinate of the aTrackRef segm
  478. * (and therefore connect the 2 other ending points)
  479. * and return aCandidate (which can be deleted).
  480. * else return NULL
  481. */
  482. static void updateConn( TRACK *track, std::shared_ptr<CONNECTIVITY_DATA> connectivity )
  483. {
  484. for( auto pad : connectivity->GetConnectedPads( track ) )
  485. {
  486. if( pad->HitTest( track->GetStart() ) )
  487. {
  488. track->SetState( START_ON_PAD, true );
  489. }
  490. if( pad->HitTest( track->GetEnd() ) )
  491. {
  492. track->SetState( END_ON_PAD, true );
  493. }
  494. }
  495. }
  496. TRACK* TRACKS_CLEANER::mergeCollinearSegmentIfPossible( TRACK* aTrackRef, TRACK* aCandidate,
  497. ENDPOINT_T aEndType )
  498. {
  499. // First of all, they must be of the same width and must be both actual tracks
  500. if( ( aTrackRef->GetWidth() != aCandidate->GetWidth() ) ||
  501. ( aTrackRef->Type() != PCB_TRACE_T ) ||
  502. ( aCandidate->Type() != PCB_TRACE_T ) )
  503. return NULL;
  504. // Trivial case: exactly the same track
  505. if( ( aTrackRef->GetStart() == aCandidate->GetStart() ) &&
  506. ( aTrackRef->GetEnd() == aCandidate->GetEnd() ) )
  507. return aCandidate;
  508. if( ( aTrackRef->GetStart() == aCandidate->GetEnd() ) &&
  509. ( aTrackRef->GetEnd() == aCandidate->GetStart() ) )
  510. return aCandidate;
  511. // Weed out non-parallel tracks
  512. if( !parallelismTest( aTrackRef->GetEnd().x - aTrackRef->GetStart().x,
  513. aTrackRef->GetEnd().y - aTrackRef->GetStart().y,
  514. aCandidate->GetEnd().x - aCandidate->GetStart().x,
  515. aCandidate->GetEnd().y - aCandidate->GetStart().y ) )
  516. return NULL;
  517. auto connectivity = m_brd->GetConnectivity();
  518. updateConn( aTrackRef, connectivity );
  519. updateConn( aCandidate, connectivity );
  520. if( aEndType == ENDPOINT_START )
  521. {
  522. // We do not have a pad, which is a always terminal point for a track
  523. if( aTrackRef->GetState( START_ON_PAD ) )
  524. return NULL;
  525. /* change the common point coordinate of pt_segm to use the other point
  526. * of pt_segm (pt_segm will be removed later) */
  527. if( aTrackRef->GetStart() == aCandidate->GetStart() )
  528. {
  529. m_commit.Modify( aTrackRef );
  530. aTrackRef->SetStart( aCandidate->GetEnd() );
  531. aTrackRef->SetState( START_ON_PAD, aCandidate->GetState( END_ON_PAD ) );
  532. connectivity->Update( aTrackRef );
  533. return aCandidate;
  534. }
  535. else
  536. {
  537. m_commit.Modify( aTrackRef );
  538. aTrackRef->SetStart( aCandidate->GetStart() );
  539. aTrackRef->SetState( START_ON_PAD, aCandidate->GetState( START_ON_PAD ) );
  540. connectivity->Update( aTrackRef );
  541. return aCandidate;
  542. }
  543. }
  544. else // aEndType == END
  545. {
  546. // We do not have a pad, which is a always terminal point for a track
  547. if( aTrackRef->GetState( END_ON_PAD ) )
  548. return NULL;
  549. /* change the common point coordinate of pt_segm to use the other point
  550. * of pt_segm (pt_segm will be removed later) */
  551. if( aTrackRef->GetEnd() == aCandidate->GetStart() )
  552. {
  553. m_commit.Modify( aTrackRef );
  554. aTrackRef->SetEnd( aCandidate->GetEnd() );
  555. aTrackRef->SetState( END_ON_PAD, aCandidate->GetState( END_ON_PAD ) );
  556. connectivity->Update( aTrackRef );
  557. return aCandidate;
  558. }
  559. else
  560. {
  561. m_commit.Modify( aTrackRef );
  562. aTrackRef->SetEnd( aCandidate->GetStart() );
  563. aTrackRef->SetState( END_ON_PAD, aCandidate->GetState( START_ON_PAD ) );
  564. connectivity->Update( aTrackRef );
  565. return aCandidate;
  566. }
  567. }
  568. return NULL;
  569. }
  570. bool PCB_EDIT_FRAME::RemoveMisConnectedTracks()
  571. {
  572. // Old model has to be refreshed, GAL normally does not keep updating it
  573. Compile_Ratsnest( NULL, false );
  574. BOARD_COMMIT commit( this );
  575. TRACKS_CLEANER cleaner( GetBoard(), commit );
  576. bool isModified = cleaner.CleanupBoard( true, false, false, false );
  577. if( isModified )
  578. {
  579. // Clear undo and redo lists to avoid inconsistencies between lists
  580. SetCurItem( NULL );
  581. commit.Push( _( "Board cleanup" ) );
  582. Compile_Ratsnest( NULL, true );
  583. }
  584. m_canvas->Refresh( true );
  585. return isModified;
  586. }