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.

1312 lines
38 KiB

4 years ago
5 years ago
8 years ago
8 years ago
8 years ago
4 years ago
  1. /*
  2. * This program source code file is part of KiCad, a free EDA CAD application.
  3. *
  4. * Copyright (C) 2017 Jean-Pierre Charras, jp.charras at wanadoo.fr
  5. * Copyright (C) 2012 SoftPLC Corporation, Dick Hollenbeck <dick@softplc.com>
  6. * Copyright (C) 1992-2022 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. #include <bitmaps.h>
  26. #include <geometry/geometry_utils.h>
  27. #include <geometry/shape_null.h>
  28. #include <pcb_edit_frame.h>
  29. #include <pcb_screen.h>
  30. #include <board.h>
  31. #include <board_design_settings.h>
  32. #include <pad.h>
  33. #include <zone.h>
  34. #include <string_utils.h>
  35. #include <math_for_graphics.h>
  36. #include <settings/color_settings.h>
  37. #include <settings/settings_manager.h>
  38. #include <trigo.h>
  39. #include <i18n_utility.h>
  40. ZONE::ZONE( BOARD_ITEM_CONTAINER* aParent, bool aInFP ) :
  41. BOARD_CONNECTED_ITEM( aParent, aInFP ? PCB_FP_ZONE_T : PCB_ZONE_T ),
  42. m_area( 0.0 ),
  43. m_outlinearea( 0.0 )
  44. {
  45. m_Poly = new SHAPE_POLY_SET(); // Outlines
  46. m_cornerSmoothingType = ZONE_SETTINGS::SMOOTHING_NONE;
  47. m_cornerRadius = 0;
  48. m_zoneName = wxEmptyString;
  49. m_CornerSelection = nullptr; // no corner is selected
  50. m_isFilled = false; // fill status : true when the zone is filled
  51. m_teardropType = TEARDROP_TYPE::TD_NONE;
  52. m_islandRemovalMode = ISLAND_REMOVAL_MODE::ALWAYS;
  53. m_borderStyle = ZONE_BORDER_DISPLAY_STYLE::DIAGONAL_EDGE;
  54. m_borderHatchPitch = GetDefaultHatchPitch();
  55. m_priority = 0;
  56. SetIsRuleArea( aInFP ); // Zones living in footprints have the rule area option
  57. SetLocalFlags( 0 ); // flags temporary used in zone calculations
  58. m_fillVersion = 5; // set the "old" way to build filled polygon areas (< 6.0.x)
  59. aParent->GetZoneSettings().ExportSetting( *this );
  60. m_ZoneMinThickness = EDA_UNIT_UTILS::Mils2IU( pcbIUScale, ZONE_THICKNESS_MIL );
  61. m_thermalReliefSpokeWidth = EDA_UNIT_UTILS::Mils2IU( pcbIUScale, ZONE_THERMAL_RELIEF_COPPER_WIDTH_MIL );
  62. m_thermalReliefGap = EDA_UNIT_UTILS::Mils2IU( pcbIUScale, ZONE_THERMAL_RELIEF_GAP_MIL );
  63. m_needRefill = false; // True only after edits.
  64. }
  65. ZONE::ZONE( const ZONE& aZone ) :
  66. BOARD_CONNECTED_ITEM( aZone ),
  67. m_Poly( nullptr ),
  68. m_CornerSelection( nullptr )
  69. {
  70. InitDataFromSrcInCopyCtor( aZone );
  71. }
  72. ZONE& ZONE::operator=( const ZONE& aOther )
  73. {
  74. BOARD_CONNECTED_ITEM::operator=( aOther );
  75. InitDataFromSrcInCopyCtor( aOther );
  76. return *this;
  77. }
  78. ZONE::~ZONE()
  79. {
  80. delete m_Poly;
  81. delete m_CornerSelection;
  82. }
  83. void ZONE::InitDataFromSrcInCopyCtor( const ZONE& aZone )
  84. {
  85. // members are expected non initialize in this.
  86. // InitDataFromSrcInCopyCtor() is expected to be called only from a copy constructor.
  87. // Copy only useful EDA_ITEM flags:
  88. m_flags = aZone.m_flags;
  89. m_forceVisible = aZone.m_forceVisible;
  90. // Replace the outlines for aZone outlines.
  91. delete m_Poly;
  92. m_Poly = new SHAPE_POLY_SET( *aZone.m_Poly );
  93. m_cornerSmoothingType = aZone.m_cornerSmoothingType;
  94. m_cornerRadius = aZone.m_cornerRadius;
  95. m_zoneName = aZone.m_zoneName;
  96. m_priority = aZone.m_priority;
  97. m_isRuleArea = aZone.m_isRuleArea;
  98. SetLayerSet( aZone.GetLayerSet() );
  99. m_doNotAllowCopperPour = aZone.m_doNotAllowCopperPour;
  100. m_doNotAllowVias = aZone.m_doNotAllowVias;
  101. m_doNotAllowTracks = aZone.m_doNotAllowTracks;
  102. m_doNotAllowPads = aZone.m_doNotAllowPads;
  103. m_doNotAllowFootprints = aZone.m_doNotAllowFootprints;
  104. m_PadConnection = aZone.m_PadConnection;
  105. m_ZoneClearance = aZone.m_ZoneClearance; // clearance value
  106. m_ZoneMinThickness = aZone.m_ZoneMinThickness;
  107. m_fillVersion = aZone.m_fillVersion;
  108. m_islandRemovalMode = aZone.m_islandRemovalMode;
  109. m_minIslandArea = aZone.m_minIslandArea;
  110. m_isFilled = aZone.m_isFilled;
  111. m_needRefill = aZone.m_needRefill;
  112. m_teardropType = aZone.m_teardropType;
  113. m_thermalReliefGap = aZone.m_thermalReliefGap;
  114. m_thermalReliefSpokeWidth = aZone.m_thermalReliefSpokeWidth;
  115. m_fillMode = aZone.m_fillMode; // solid vs. hatched
  116. m_hatchThickness = aZone.m_hatchThickness;
  117. m_hatchGap = aZone.m_hatchGap;
  118. m_hatchOrientation = aZone.m_hatchOrientation;
  119. m_hatchSmoothingLevel = aZone.m_hatchSmoothingLevel;
  120. m_hatchSmoothingValue = aZone.m_hatchSmoothingValue;
  121. m_hatchBorderAlgorithm = aZone.m_hatchBorderAlgorithm;
  122. m_hatchHoleMinArea = aZone.m_hatchHoleMinArea;
  123. // For corner moving, corner index to drag, or nullptr if no selection
  124. delete m_CornerSelection;
  125. m_CornerSelection = nullptr;
  126. for( PCB_LAYER_ID layer : aZone.GetLayerSet().Seq() )
  127. {
  128. std::shared_ptr<SHAPE_POLY_SET> fill = aZone.m_FilledPolysList.at( layer );
  129. if( fill )
  130. m_FilledPolysList[layer] = std::make_shared<SHAPE_POLY_SET>( *fill );
  131. else
  132. m_FilledPolysList[layer] = std::make_shared<SHAPE_POLY_SET>();
  133. m_filledPolysHash[layer] = aZone.m_filledPolysHash.at( layer );
  134. m_insulatedIslands[layer] = aZone.m_insulatedIslands.at( layer );
  135. }
  136. m_borderStyle = aZone.m_borderStyle;
  137. m_borderHatchPitch = aZone.m_borderHatchPitch;
  138. m_borderHatchLines = aZone.m_borderHatchLines;
  139. SetLocalFlags( aZone.GetLocalFlags() );
  140. m_netinfo = aZone.m_netinfo;
  141. m_area = aZone.m_area;
  142. m_outlinearea = aZone.m_outlinearea;
  143. }
  144. EDA_ITEM* ZONE::Clone() const
  145. {
  146. return new ZONE( *this );
  147. }
  148. bool ZONE::HigherPriority( const ZONE* aOther ) const
  149. {
  150. // Teardrops are always higher priority than regular zones, so if one zone is a teardrop
  151. // and the other is not, then return higher priority as the teardrop
  152. if( ( m_teardropType == TEARDROP_TYPE::TD_NONE )
  153. ^ ( aOther->m_teardropType == TEARDROP_TYPE::TD_NONE ) )
  154. {
  155. return static_cast<int>( m_teardropType ) > static_cast<int>( aOther->m_teardropType );
  156. }
  157. if( m_priority != aOther->m_priority )
  158. return m_priority > aOther->m_priority;
  159. return m_Uuid > aOther->m_Uuid;
  160. }
  161. bool ZONE::SameNet( const ZONE* aOther ) const
  162. {
  163. return GetNetCode() == aOther->GetNetCode();
  164. }
  165. bool ZONE::UnFill()
  166. {
  167. bool change = false;
  168. for( std::pair<const PCB_LAYER_ID, std::shared_ptr<SHAPE_POLY_SET>>& pair : m_FilledPolysList )
  169. {
  170. change |= !pair.second->IsEmpty();
  171. m_insulatedIslands[pair.first].clear();
  172. pair.second->RemoveAllContours();
  173. }
  174. m_isFilled = false;
  175. m_fillFlags.reset();
  176. return change;
  177. }
  178. VECTOR2I ZONE::GetPosition() const
  179. {
  180. return GetCornerPosition( 0 );
  181. }
  182. PCB_LAYER_ID ZONE::GetLayer() const
  183. {
  184. return BOARD_ITEM::GetLayer();
  185. }
  186. PCB_LAYER_ID ZONE::GetFirstLayer() const
  187. {
  188. if( m_layerSet.size() )
  189. return m_layerSet.UIOrder()[0];
  190. else
  191. return UNDEFINED_LAYER;
  192. }
  193. bool ZONE::IsOnCopperLayer() const
  194. {
  195. return ( m_layerSet & LSET::AllCuMask() ).count() > 0;
  196. }
  197. void ZONE::SetLayer( PCB_LAYER_ID aLayer )
  198. {
  199. SetLayerSet( LSET( aLayer ) );
  200. }
  201. void ZONE::SetLayerSet( LSET aLayerSet )
  202. {
  203. if( aLayerSet.count() == 0 )
  204. return;
  205. if( m_layerSet != aLayerSet )
  206. {
  207. SetNeedRefill( true );
  208. UnFill();
  209. m_FilledPolysList.clear();
  210. m_filledPolysHash.clear();
  211. m_insulatedIslands.clear();
  212. for( PCB_LAYER_ID layer : aLayerSet.Seq() )
  213. {
  214. m_FilledPolysList[layer] = std::make_shared<SHAPE_POLY_SET>();
  215. m_filledPolysHash[layer] = {};
  216. m_insulatedIslands[layer] = {};
  217. }
  218. }
  219. m_layerSet = aLayerSet;
  220. }
  221. void ZONE::ViewGetLayers( int aLayers[], int& aCount ) const
  222. {
  223. LSEQ layers = m_layerSet.Seq();
  224. for( unsigned int idx = 0; idx < layers.size(); idx++ )
  225. aLayers[idx] = LAYER_ZONE_START + layers[idx];
  226. aCount = layers.size();
  227. }
  228. double ZONE::ViewGetLOD( int aLayer, KIGFX::VIEW* aView ) const
  229. {
  230. constexpr double HIDE = std::numeric_limits<double>::max();
  231. return aView->IsLayerVisible( LAYER_ZONES ) ? 0.0 : HIDE;
  232. }
  233. bool ZONE::IsOnLayer( PCB_LAYER_ID aLayer ) const
  234. {
  235. return m_layerSet.test( aLayer );
  236. }
  237. const BOX2I ZONE::GetBoundingBox() const
  238. {
  239. return m_Poly->BBox();
  240. }
  241. int ZONE::GetThermalReliefGap( PAD* aPad, wxString* aSource ) const
  242. {
  243. if( aPad->GetLocalThermalGapOverride() == 0 )
  244. {
  245. if( aSource )
  246. *aSource = _( "zone" );
  247. return m_thermalReliefGap;
  248. }
  249. return aPad->GetLocalThermalGapOverride( aSource );
  250. }
  251. void ZONE::SetCornerRadius( unsigned int aRadius )
  252. {
  253. if( m_cornerRadius != aRadius )
  254. SetNeedRefill( true );
  255. m_cornerRadius = aRadius;
  256. }
  257. static SHAPE_POLY_SET g_nullPoly;
  258. MD5_HASH ZONE::GetHashValue( PCB_LAYER_ID aLayer )
  259. {
  260. if( !m_filledPolysHash.count( aLayer ) )
  261. return g_nullPoly.GetHash();
  262. else
  263. return m_filledPolysHash.at( aLayer );
  264. }
  265. void ZONE::BuildHashValue( PCB_LAYER_ID aLayer )
  266. {
  267. if( !m_FilledPolysList.count( aLayer ) )
  268. m_filledPolysHash[aLayer] = g_nullPoly.GetHash();
  269. else
  270. m_filledPolysHash[aLayer] = m_FilledPolysList.at( aLayer )->GetHash();
  271. }
  272. bool ZONE::HitTest( const VECTOR2I& aPosition, int aAccuracy ) const
  273. {
  274. // When looking for an "exact" hit aAccuracy will be 0 which works poorly for very thin
  275. // lines. Give it a floor.
  276. int accuracy = std::max( aAccuracy, pcbIUScale.mmToIU( 0.1 ) );
  277. return HitTestForCorner( aPosition, accuracy * 2 ) || HitTestForEdge( aPosition, accuracy );
  278. }
  279. bool ZONE::HitTestForCorner( const VECTOR2I& refPos, int aAccuracy,
  280. SHAPE_POLY_SET::VERTEX_INDEX* aCornerHit ) const
  281. {
  282. return m_Poly->CollideVertex( VECTOR2I( refPos ), aCornerHit, aAccuracy );
  283. }
  284. bool ZONE::HitTestForEdge( const VECTOR2I& refPos, int aAccuracy,
  285. SHAPE_POLY_SET::VERTEX_INDEX* aCornerHit ) const
  286. {
  287. return m_Poly->CollideEdge( VECTOR2I( refPos ), aCornerHit, aAccuracy );
  288. }
  289. bool ZONE::HitTest( const BOX2I& aRect, bool aContained, int aAccuracy ) const
  290. {
  291. // Calculate bounding box for zone
  292. BOX2I bbox = GetBoundingBox();
  293. bbox.Normalize();
  294. BOX2I arect = aRect;
  295. arect.Normalize();
  296. arect.Inflate( aAccuracy );
  297. if( aContained )
  298. {
  299. return arect.Contains( bbox );
  300. }
  301. else
  302. {
  303. // Fast test: if aBox is outside the polygon bounding box, rectangles cannot intersect
  304. if( !arect.Intersects( bbox ) )
  305. return false;
  306. int count = m_Poly->TotalVertices();
  307. for( int ii = 0; ii < count; ii++ )
  308. {
  309. VECTOR2I vertex = m_Poly->CVertex( ii );
  310. VECTOR2I vertexNext = m_Poly->CVertex( ( ii + 1 ) % count );
  311. // Test if the point is within the rect
  312. if( arect.Contains( vertex ) )
  313. return true;
  314. // Test if this edge intersects the rect
  315. if( arect.Intersects( vertex, vertexNext ) )
  316. return true;
  317. }
  318. return false;
  319. }
  320. }
  321. int ZONE::GetLocalClearance( wxString* aSource ) const
  322. {
  323. if( m_isRuleArea )
  324. return 0;
  325. if( aSource )
  326. *aSource = _( "zone" );
  327. return m_ZoneClearance;
  328. }
  329. bool ZONE::HitTestFilledArea( PCB_LAYER_ID aLayer, const VECTOR2I& aRefPos, int aAccuracy ) const
  330. {
  331. // Rule areas have no filled area, but it's generally nice to treat their interior as if it were
  332. // filled so that people don't have to select them by their outline (which is min-width)
  333. if( GetIsRuleArea() )
  334. return m_Poly->Contains( aRefPos, -1, aAccuracy );
  335. if( !m_FilledPolysList.count( aLayer ) )
  336. return false;
  337. return m_FilledPolysList.at( aLayer )->Contains( aRefPos, -1, aAccuracy );
  338. }
  339. bool ZONE::HitTestCutout( const VECTOR2I& aRefPos, int* aOutlineIdx, int* aHoleIdx ) const
  340. {
  341. // Iterate over each outline polygon in the zone and then iterate over
  342. // each hole it has to see if the point is in it.
  343. for( int i = 0; i < m_Poly->OutlineCount(); i++ )
  344. {
  345. for( int j = 0; j < m_Poly->HoleCount( i ); j++ )
  346. {
  347. if( m_Poly->Hole( i, j ).PointInside( aRefPos ) )
  348. {
  349. if( aOutlineIdx )
  350. *aOutlineIdx = i;
  351. if( aHoleIdx )
  352. *aHoleIdx = j;
  353. return true;
  354. }
  355. }
  356. }
  357. return false;
  358. }
  359. void ZONE::GetMsgPanelInfo( EDA_DRAW_FRAME* aFrame, std::vector<MSG_PANEL_ITEM>& aList )
  360. {
  361. wxString msg;
  362. if( GetIsRuleArea() )
  363. msg = _( "Rule Area" );
  364. else if( IsTeardropArea() )
  365. msg = _( "Teardrop Area" );
  366. else if( IsOnCopperLayer() )
  367. msg = _( "Copper Zone" );
  368. else
  369. msg = _( "Non-copper Zone" );
  370. // Display Cutout instead of Outline for holes inside a zone (i.e. when num contour !=0).
  371. // Check whether the selected corner is in a hole; i.e., in any contour but the first one.
  372. if( m_CornerSelection != nullptr && m_CornerSelection->m_contour > 0 )
  373. msg << wxT( " " ) << _( "Cutout" );
  374. aList.emplace_back( _( "Type" ), msg );
  375. if( GetIsRuleArea() )
  376. {
  377. msg.Empty();
  378. if( GetDoNotAllowVias() )
  379. AccumulateDescription( msg, _( "No vias" ) );
  380. if( GetDoNotAllowTracks() )
  381. AccumulateDescription( msg, _( "No tracks" ) );
  382. if( GetDoNotAllowPads() )
  383. AccumulateDescription( msg, _( "No pads" ) );
  384. if( GetDoNotAllowCopperPour() )
  385. AccumulateDescription( msg, _( "No copper zones" ) );
  386. if( GetDoNotAllowFootprints() )
  387. AccumulateDescription( msg, _( "No footprints" ) );
  388. if( !msg.IsEmpty() )
  389. aList.emplace_back( _( "Restrictions" ), msg );
  390. }
  391. else if( IsOnCopperLayer() )
  392. {
  393. if( aFrame->GetName() == PCB_EDIT_FRAME_NAME )
  394. {
  395. aList.emplace_back( _( "Net" ), UnescapeString( GetNetname() ) );
  396. aList.emplace_back( _( "Resolved Netclass" ),
  397. UnescapeString( GetEffectiveNetClass()->GetName() ) );
  398. }
  399. // Display priority level
  400. aList.emplace_back( _( "Priority" ),
  401. wxString::Format( wxT( "%d" ), GetAssignedPriority() ) );
  402. }
  403. if( aFrame->GetName() == PCB_EDIT_FRAME_NAME )
  404. {
  405. if( IsLocked() )
  406. aList.emplace_back( _( "Status" ), _( "Locked" ) );
  407. }
  408. wxString layerDesc;
  409. int count = 0;
  410. for( PCB_LAYER_ID layer : m_layerSet.Seq() )
  411. {
  412. if( count == 0 )
  413. layerDesc = GetBoard()->GetLayerName( layer );
  414. count++;
  415. }
  416. if( count > 1 )
  417. layerDesc.Printf( _( "%s and %d more" ), layerDesc, count - 1 );
  418. aList.emplace_back( _( "Layer" ), layerDesc );
  419. if( !m_zoneName.empty() )
  420. aList.emplace_back( _( "Name" ), m_zoneName );
  421. switch( m_fillMode )
  422. {
  423. case ZONE_FILL_MODE::POLYGONS: msg = _( "Solid" ); break;
  424. case ZONE_FILL_MODE::HATCH_PATTERN: msg = _( "Hatched" ); break;
  425. default: msg = _( "Unknown" ); break;
  426. }
  427. aList.emplace_back( _( "Fill Mode" ), msg );
  428. aList.emplace_back( _( "Filled Area" ),
  429. aFrame->MessageTextFromValue( m_area, true, EDA_DATA_TYPE::AREA ) );
  430. wxString source;
  431. int clearance = GetOwnClearance( UNDEFINED_LAYER, &source );
  432. if( !source.IsEmpty() )
  433. {
  434. aList.emplace_back( wxString::Format( _( "Min Clearance: %s" ),
  435. aFrame->MessageTextFromValue( clearance ) ),
  436. wxString::Format( _( "(from %s)" ),
  437. source ) );
  438. }
  439. if( !m_FilledPolysList.empty() )
  440. {
  441. count = 0;
  442. for( std::pair<const PCB_LAYER_ID, std::shared_ptr<SHAPE_POLY_SET>>& ii: m_FilledPolysList )
  443. count += ii.second->TotalVertices();
  444. aList.emplace_back( _( "Corner Count" ), wxString::Format( wxT( "%d" ), count ) );
  445. }
  446. }
  447. void ZONE::Move( const VECTOR2I& offset )
  448. {
  449. /* move outlines */
  450. m_Poly->Move( offset );
  451. HatchBorder();
  452. for( std::pair<const PCB_LAYER_ID, std::shared_ptr<SHAPE_POLY_SET>>& pair : m_FilledPolysList )
  453. pair.second->Move( offset );
  454. }
  455. void ZONE::MoveEdge( const VECTOR2I& offset, int aEdge )
  456. {
  457. int next_corner;
  458. if( m_Poly->GetNeighbourIndexes( aEdge, nullptr, &next_corner ) )
  459. {
  460. m_Poly->SetVertex( aEdge, m_Poly->CVertex( aEdge ) + VECTOR2I( offset ) );
  461. m_Poly->SetVertex( next_corner, m_Poly->CVertex( next_corner ) + VECTOR2I( offset ) );
  462. HatchBorder();
  463. SetNeedRefill( true );
  464. }
  465. }
  466. void ZONE::Rotate( const VECTOR2I& aCentre, const EDA_ANGLE& aAngle )
  467. {
  468. m_Poly->Rotate( aAngle, VECTOR2I( aCentre ) );
  469. HatchBorder();
  470. /* rotate filled areas: */
  471. for( std::pair<const PCB_LAYER_ID, std::shared_ptr<SHAPE_POLY_SET>>& pair : m_FilledPolysList )
  472. pair.second->Rotate( aAngle, aCentre );
  473. }
  474. void ZONE::Flip( const VECTOR2I& aCentre, bool aFlipLeftRight )
  475. {
  476. Mirror( aCentre, aFlipLeftRight );
  477. SetLayerSet( FlipLayerMask( GetLayerSet(), GetBoard()->GetCopperLayerCount() ) );
  478. }
  479. void ZONE::Mirror( const VECTOR2I& aMirrorRef, bool aMirrorLeftRight )
  480. {
  481. m_Poly->Mirror( aMirrorLeftRight, !aMirrorLeftRight, aMirrorRef );
  482. HatchBorder();
  483. for( std::pair<const PCB_LAYER_ID, std::shared_ptr<SHAPE_POLY_SET>>& pair : m_FilledPolysList )
  484. pair.second->Mirror( aMirrorLeftRight, !aMirrorLeftRight, aMirrorRef );
  485. }
  486. void ZONE::RemoveCutout( int aOutlineIdx, int aHoleIdx )
  487. {
  488. // Ensure the requested cutout is valid
  489. if( m_Poly->OutlineCount() < aOutlineIdx || m_Poly->HoleCount( aOutlineIdx ) < aHoleIdx )
  490. return;
  491. SHAPE_POLY_SET cutPoly( m_Poly->Hole( aOutlineIdx, aHoleIdx ) );
  492. // Add the cutout back to the zone
  493. m_Poly->BooleanAdd( cutPoly, SHAPE_POLY_SET::PM_FAST );
  494. SetNeedRefill( true );
  495. }
  496. void ZONE::AddPolygon( const SHAPE_LINE_CHAIN& aPolygon )
  497. {
  498. wxASSERT( aPolygon.IsClosed() );
  499. // Add the outline as a new polygon in the polygon set
  500. if( m_Poly->OutlineCount() == 0 )
  501. m_Poly->AddOutline( aPolygon );
  502. else
  503. m_Poly->AddHole( aPolygon );
  504. SetNeedRefill( true );
  505. }
  506. void ZONE::AddPolygon( std::vector<VECTOR2I>& aPolygon )
  507. {
  508. if( aPolygon.empty() )
  509. return;
  510. SHAPE_LINE_CHAIN outline;
  511. // Create an outline and populate it with the points of aPolygon
  512. for( const VECTOR2I& pt : aPolygon )
  513. outline.Append( pt );
  514. outline.SetClosed( true );
  515. AddPolygon( outline );
  516. }
  517. bool ZONE::AppendCorner( VECTOR2I aPosition, int aHoleIdx, bool aAllowDuplication )
  518. {
  519. // Ensure the main outline exists:
  520. if( m_Poly->OutlineCount() == 0 )
  521. m_Poly->NewOutline();
  522. // If aHoleIdx >= 0, the corner musty be added to the hole, index aHoleIdx.
  523. // (remember: the index of the first hole is 0)
  524. // Return error if it does not exist.
  525. if( aHoleIdx >= m_Poly->HoleCount( 0 ) )
  526. return false;
  527. m_Poly->Append( aPosition.x, aPosition.y, -1, aHoleIdx, aAllowDuplication );
  528. SetNeedRefill( true );
  529. return true;
  530. }
  531. wxString ZONE::GetSelectMenuText( UNITS_PROVIDER* aUnitsProvider ) const
  532. {
  533. wxString layerDesc;
  534. int count = 0;
  535. for( PCB_LAYER_ID layer : m_layerSet.Seq() )
  536. {
  537. if( count == 0 )
  538. layerDesc = GetBoard()->GetLayerName( layer );
  539. count++;
  540. }
  541. if( count > 1 )
  542. layerDesc.Printf( _( "%s and %d more" ), layerDesc, count - 1 );
  543. // Check whether the selected contour is a hole (contour index > 0)
  544. if( m_CornerSelection != nullptr && m_CornerSelection->m_contour > 0 )
  545. {
  546. if( GetIsRuleArea() )
  547. return wxString::Format( _( "Rule Area Cutout on %s" ), layerDesc );
  548. else
  549. return wxString::Format( _( "Zone Cutout on %s" ), layerDesc );
  550. }
  551. else
  552. {
  553. if( GetIsRuleArea() )
  554. return wxString::Format( _( "Rule Area on %s" ), layerDesc );
  555. else
  556. return wxString::Format( _( "Zone %s on %s" ), GetNetnameMsg(), layerDesc );
  557. }
  558. }
  559. int ZONE::GetBorderHatchPitch() const
  560. {
  561. return m_borderHatchPitch;
  562. }
  563. void ZONE::SetBorderDisplayStyle( ZONE_BORDER_DISPLAY_STYLE aBorderHatchStyle,
  564. int aBorderHatchPitch, bool aRebuildBorderHatch )
  565. {
  566. aBorderHatchPitch = std::max( aBorderHatchPitch,
  567. pcbIUScale.mmToIU( ZONE_BORDER_HATCH_MINDIST_MM ) );
  568. aBorderHatchPitch = std::min( aBorderHatchPitch,
  569. pcbIUScale.mmToIU( ZONE_BORDER_HATCH_MAXDIST_MM ) );
  570. SetBorderHatchPitch( aBorderHatchPitch );
  571. m_borderStyle = aBorderHatchStyle;
  572. if( aRebuildBorderHatch )
  573. HatchBorder();
  574. }
  575. void ZONE::SetBorderHatchPitch( int aPitch )
  576. {
  577. m_borderHatchPitch = aPitch;
  578. }
  579. void ZONE::UnHatchBorder()
  580. {
  581. m_borderHatchLines.clear();
  582. }
  583. // Creates hatch lines inside the outline of the complex polygon
  584. // sort function used in ::HatchBorder to sort points by descending VECTOR2I.x values
  585. bool sortEndsByDescendingX( const VECTOR2I& ref, const VECTOR2I& tst )
  586. {
  587. return tst.x < ref.x;
  588. }
  589. void ZONE::HatchBorder()
  590. {
  591. UnHatchBorder();
  592. if( m_borderStyle == ZONE_BORDER_DISPLAY_STYLE::NO_HATCH
  593. || m_borderHatchPitch == 0
  594. || m_Poly->IsEmpty() )
  595. {
  596. return;
  597. }
  598. // define range for hatch lines
  599. int min_x = m_Poly->CVertex( 0 ).x;
  600. int max_x = m_Poly->CVertex( 0 ).x;
  601. int min_y = m_Poly->CVertex( 0 ).y;
  602. int max_y = m_Poly->CVertex( 0 ).y;
  603. for( auto iterator = m_Poly->IterateWithHoles(); iterator; iterator++ )
  604. {
  605. if( iterator->x < min_x )
  606. min_x = iterator->x;
  607. if( iterator->x > max_x )
  608. max_x = iterator->x;
  609. if( iterator->y < min_y )
  610. min_y = iterator->y;
  611. if( iterator->y > max_y )
  612. max_y = iterator->y;
  613. }
  614. // Calculate spacing between 2 hatch lines
  615. int spacing;
  616. if( m_borderStyle == ZONE_BORDER_DISPLAY_STYLE::DIAGONAL_EDGE )
  617. spacing = m_borderHatchPitch;
  618. else
  619. spacing = m_borderHatchPitch * 2;
  620. // set the "length" of hatch lines (the length on horizontal axis)
  621. int hatch_line_len = m_borderHatchPitch;
  622. // To have a better look, give a slope depending on the layer
  623. int layer = GetFirstLayer();
  624. int slope_flag = (layer & 1) ? 1 : -1; // 1 or -1
  625. double slope = 0.707106 * slope_flag; // 45 degrees slope
  626. int max_a, min_a;
  627. if( slope_flag == 1 )
  628. {
  629. max_a = KiROUND( max_y - slope * min_x );
  630. min_a = KiROUND( min_y - slope * max_x );
  631. }
  632. else
  633. {
  634. max_a = KiROUND( max_y - slope * max_x );
  635. min_a = KiROUND( min_y - slope * min_x );
  636. }
  637. min_a = (min_a / spacing) * spacing;
  638. // calculate an offset depending on layer number,
  639. // for a better look of hatches on a multilayer board
  640. int offset = (layer * 7) / 8;
  641. min_a += offset;
  642. // loop through hatch lines
  643. std::vector<VECTOR2I> pointbuffer;
  644. pointbuffer.reserve( 256 );
  645. for( int a = min_a; a < max_a; a += spacing )
  646. {
  647. pointbuffer.clear();
  648. // Iterate through all vertices
  649. for( auto iterator = m_Poly->IterateSegmentsWithHoles(); iterator; iterator++ )
  650. {
  651. double x, y;
  652. SEG segment = *iterator;
  653. if( FindLineSegmentIntersection( a, slope, segment.A.x, segment.A.y, segment.B.x,
  654. segment.B.y, x, y ) )
  655. pointbuffer.emplace_back( KiROUND( x ), KiROUND( y ) );
  656. }
  657. // sort points in order of descending x (if more than 2) to
  658. // ensure the starting point and the ending point of the same segment
  659. // are stored one just after the other.
  660. if( pointbuffer.size() > 2 )
  661. sort( pointbuffer.begin(), pointbuffer.end(), sortEndsByDescendingX );
  662. // creates lines or short segments inside the complex polygon
  663. for( size_t ip = 0; ip + 1 < pointbuffer.size(); ip += 2 )
  664. {
  665. int dx = pointbuffer[ip + 1].x - pointbuffer[ip].x;
  666. // Push only one line for diagonal hatch,
  667. // or for small lines < twice the line length
  668. // else push 2 small lines
  669. if( m_borderStyle == ZONE_BORDER_DISPLAY_STYLE::DIAGONAL_FULL
  670. || std::abs( dx ) < 2 * hatch_line_len )
  671. {
  672. m_borderHatchLines.emplace_back( SEG( pointbuffer[ip], pointbuffer[ ip + 1] ) );
  673. }
  674. else
  675. {
  676. double dy = pointbuffer[ip + 1].y - pointbuffer[ip].y;
  677. slope = dy / dx;
  678. if( dx > 0 )
  679. dx = hatch_line_len;
  680. else
  681. dx = -hatch_line_len;
  682. int x1 = KiROUND( pointbuffer[ip].x + dx );
  683. int x2 = KiROUND( pointbuffer[ip + 1].x - dx );
  684. int y1 = KiROUND( pointbuffer[ip].y + dx * slope );
  685. int y2 = KiROUND( pointbuffer[ip + 1].y - dx * slope );
  686. m_borderHatchLines.emplace_back( SEG( pointbuffer[ip].x, pointbuffer[ip].y,
  687. x1, y1 ) );
  688. m_borderHatchLines.emplace_back( SEG( pointbuffer[ip+1].x, pointbuffer[ip+1].y,
  689. x2, y2 ) );
  690. }
  691. }
  692. }
  693. }
  694. int ZONE::GetDefaultHatchPitch()
  695. {
  696. return EDA_UNIT_UTILS::Mils2IU( pcbIUScale, ZONE_BORDER_HATCH_DIST_MIL );
  697. }
  698. BITMAPS ZONE::GetMenuImage() const
  699. {
  700. return BITMAPS::add_zone;
  701. }
  702. void ZONE::SwapData( BOARD_ITEM* aImage )
  703. {
  704. assert( aImage->Type() == PCB_ZONE_T || aImage->Type() == PCB_FP_ZONE_T );
  705. std::swap( *((ZONE*) this), *((ZONE*) aImage) );
  706. }
  707. void ZONE::CacheTriangulation( PCB_LAYER_ID aLayer )
  708. {
  709. if( aLayer == UNDEFINED_LAYER )
  710. {
  711. for( std::pair<const PCB_LAYER_ID, std::shared_ptr<SHAPE_POLY_SET>>& pair : m_FilledPolysList )
  712. pair.second->CacheTriangulation();
  713. m_Poly->CacheTriangulation( false );
  714. }
  715. else
  716. {
  717. if( m_FilledPolysList.count( aLayer ) )
  718. m_FilledPolysList[ aLayer ]->CacheTriangulation();
  719. }
  720. }
  721. bool ZONE::IsIsland( PCB_LAYER_ID aLayer, int aPolyIdx ) const
  722. {
  723. if( GetNetCode() < 1 )
  724. return true;
  725. if( !m_insulatedIslands.count( aLayer ) )
  726. return false;
  727. return m_insulatedIslands.at( aLayer ).count( aPolyIdx );
  728. }
  729. void ZONE::GetInteractingZones( PCB_LAYER_ID aLayer, std::vector<ZONE*>* aZones ) const
  730. {
  731. int epsilon = pcbIUScale.mmToIU( 0.001 );
  732. BOX2I bbox = GetCachedBoundingBox();
  733. bbox.Inflate( epsilon );
  734. for( ZONE* candidate : GetBoard()->Zones() )
  735. {
  736. if( candidate == this )
  737. continue;
  738. if( !candidate->GetLayerSet().test( aLayer ) )
  739. continue;
  740. if( candidate->GetIsRuleArea() )
  741. continue;
  742. if( candidate->GetNetCode() != GetNetCode() )
  743. continue;
  744. if( !candidate->GetCachedBoundingBox().Intersects( bbox ) )
  745. continue;
  746. for( auto iter = m_Poly->CIterate(); iter; iter++ )
  747. {
  748. if( candidate->m_Poly->Collide( iter.Get(), epsilon ) )
  749. {
  750. aZones->push_back( candidate );
  751. break;
  752. }
  753. }
  754. }
  755. }
  756. bool ZONE::BuildSmoothedPoly( SHAPE_POLY_SET& aSmoothedPoly, PCB_LAYER_ID aLayer,
  757. SHAPE_POLY_SET* aBoardOutline,
  758. SHAPE_POLY_SET* aSmoothedPolyWithApron ) const
  759. {
  760. if( GetNumCorners() <= 2 ) // malformed zone. polygon calculations will not like it ...
  761. return false;
  762. // Processing of arc shapes in zones is not yet supported because Clipper can't do boolean
  763. // operations on them. The poly outline must be converted to segments first.
  764. SHAPE_POLY_SET flattened = m_Poly->CloneDropTriangulation();
  765. flattened.ClearArcs();
  766. if( GetIsRuleArea() )
  767. {
  768. // We like keepouts just the way they are....
  769. aSmoothedPoly = flattened;
  770. return true;
  771. }
  772. const BOARD* board = GetBoard();
  773. int maxError = ARC_HIGH_DEF;
  774. bool keepExternalFillets = false;
  775. bool smooth_requested = m_cornerSmoothingType == ZONE_SETTINGS::SMOOTHING_CHAMFER
  776. || m_cornerSmoothingType == ZONE_SETTINGS::SMOOTHING_FILLET;
  777. if( IsTeardropArea() ) // We use teardrop shapes with no smoothing
  778. // these shapes are already optimized
  779. smooth_requested = false;
  780. if( board )
  781. {
  782. BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
  783. maxError = bds.m_MaxError;
  784. keepExternalFillets = bds.m_ZoneKeepExternalFillets;
  785. }
  786. auto smooth = [&]( SHAPE_POLY_SET& aPoly )
  787. {
  788. if( !smooth_requested )
  789. return;
  790. switch( m_cornerSmoothingType )
  791. {
  792. case ZONE_SETTINGS::SMOOTHING_CHAMFER:
  793. aPoly = aPoly.Chamfer( (int) m_cornerRadius );
  794. break;
  795. case ZONE_SETTINGS::SMOOTHING_FILLET:
  796. {
  797. aPoly = aPoly.Fillet( (int) m_cornerRadius, maxError );
  798. break;
  799. }
  800. default:
  801. break;
  802. }
  803. };
  804. std::vector<ZONE*> interactingZones;
  805. GetInteractingZones( aLayer, &interactingZones );
  806. SHAPE_POLY_SET* maxExtents = &flattened;
  807. SHAPE_POLY_SET withFillets;
  808. aSmoothedPoly = flattened;
  809. // Should external fillets (that is, those applied to concave corners) be kept? While it
  810. // seems safer to never have copper extend outside the zone outline, 5.1.x and prior did
  811. // indeed fill them so we leave the mode available.
  812. if( keepExternalFillets && smooth_requested )
  813. {
  814. withFillets = flattened;
  815. smooth( withFillets );
  816. withFillets.BooleanAdd( flattened, SHAPE_POLY_SET::PM_FAST );
  817. maxExtents = &withFillets;
  818. }
  819. for( ZONE* zone : interactingZones )
  820. {
  821. SHAPE_POLY_SET flattened_outline = zone->Outline()->CloneDropTriangulation();
  822. flattened_outline.ClearArcs();
  823. aSmoothedPoly.BooleanAdd( flattened_outline, SHAPE_POLY_SET::PM_FAST );
  824. }
  825. if( aBoardOutline )
  826. aSmoothedPoly.BooleanIntersection( *aBoardOutline, SHAPE_POLY_SET::PM_STRICTLY_SIMPLE );
  827. smooth( aSmoothedPoly );
  828. if( aSmoothedPolyWithApron )
  829. {
  830. SHAPE_POLY_SET poly = maxExtents->CloneDropTriangulation();
  831. poly.Inflate( m_ZoneMinThickness, 64 );
  832. *aSmoothedPolyWithApron = aSmoothedPoly;
  833. aSmoothedPolyWithApron->BooleanIntersection( poly, SHAPE_POLY_SET::PM_FAST );
  834. }
  835. aSmoothedPoly.BooleanIntersection( *maxExtents, SHAPE_POLY_SET::PM_FAST );
  836. return true;
  837. }
  838. double ZONE::CalculateFilledArea()
  839. {
  840. m_area = 0.0;
  841. // Iterate over each outline polygon in the zone and then iterate over
  842. // each hole it has to compute the total area.
  843. for( std::pair<const PCB_LAYER_ID, std::shared_ptr<SHAPE_POLY_SET>>& pair : m_FilledPolysList )
  844. {
  845. std::shared_ptr<SHAPE_POLY_SET>& poly = pair.second;
  846. for( int i = 0; i < poly->OutlineCount(); i++ )
  847. {
  848. m_area += poly->Outline( i ).Area();
  849. for( int j = 0; j < poly->HoleCount( i ); j++ )
  850. m_area -= poly->Hole( i, j ).Area();
  851. }
  852. }
  853. return m_area;
  854. }
  855. double ZONE::CalculateOutlineArea()
  856. {
  857. m_outlinearea = std::abs( m_Poly->Area() );
  858. return m_outlinearea;
  859. }
  860. void ZONE::TransformSmoothedOutlineToPolygon( SHAPE_POLY_SET& aCornerBuffer, int aClearance,
  861. int aMaxError, ERROR_LOC aErrorLoc,
  862. SHAPE_POLY_SET* aBoardOutline ) const
  863. {
  864. // Creates the zone outline polygon (with holes if any)
  865. SHAPE_POLY_SET polybuffer;
  866. // TODO: using GetFirstLayer() means it only works for single-layer zones....
  867. BuildSmoothedPoly( polybuffer, GetFirstLayer(), aBoardOutline );
  868. // Calculate the polygon with clearance
  869. // holes are linked to the main outline, so only one polygon is created.
  870. if( aClearance )
  871. {
  872. const BOARD* board = GetBoard();
  873. int maxError = ARC_HIGH_DEF;
  874. if( board )
  875. maxError = board->GetDesignSettings().m_MaxError;
  876. int segCount = GetArcToSegmentCount( aClearance, maxError, FULL_CIRCLE );
  877. if( aErrorLoc == ERROR_OUTSIDE )
  878. aClearance += aMaxError;
  879. polybuffer.Inflate( aClearance, segCount );
  880. }
  881. polybuffer.Fracture( SHAPE_POLY_SET::PM_FAST );
  882. aCornerBuffer.Append( polybuffer );
  883. }
  884. FP_ZONE::FP_ZONE( BOARD_ITEM_CONTAINER* aParent ) :
  885. ZONE( aParent, true )
  886. {
  887. // in a footprint, net classes are not managed.
  888. // so set the net to NETINFO_LIST::ORPHANED_ITEM
  889. SetNetCode( -1, true );
  890. }
  891. FP_ZONE::FP_ZONE( const FP_ZONE& aZone ) :
  892. ZONE( aZone )
  893. {
  894. InitDataFromSrcInCopyCtor( aZone );
  895. }
  896. FP_ZONE& FP_ZONE::operator=( const FP_ZONE& aOther )
  897. {
  898. ZONE::operator=( aOther );
  899. return *this;
  900. }
  901. EDA_ITEM* FP_ZONE::Clone() const
  902. {
  903. return new FP_ZONE( *this );
  904. }
  905. double FP_ZONE::ViewGetLOD( int aLayer, KIGFX::VIEW* aView ) const
  906. {
  907. constexpr double HIDE = (double)std::numeric_limits<double>::max();
  908. if( !aView )
  909. return 0;
  910. if( !aView->IsLayerVisible( LAYER_ZONES ) )
  911. return HIDE;
  912. bool flipped = GetParent() && GetParent()->GetLayer() == B_Cu;
  913. // Handle Render tab switches
  914. if( !flipped && !aView->IsLayerVisible( LAYER_MOD_FR ) )
  915. return HIDE;
  916. if( flipped && !aView->IsLayerVisible( LAYER_MOD_BK ) )
  917. return HIDE;
  918. // Other layers are shown without any conditions
  919. return 0.0;
  920. }
  921. std::shared_ptr<SHAPE> ZONE::GetEffectiveShape( PCB_LAYER_ID aLayer, FLASHING aFlash ) const
  922. {
  923. if( m_FilledPolysList.find( aLayer ) == m_FilledPolysList.end() )
  924. return std::make_shared<SHAPE_NULL>();
  925. else
  926. return m_FilledPolysList.at( aLayer );
  927. }
  928. void ZONE::TransformShapeWithClearanceToPolygon( SHAPE_POLY_SET& aCornerBuffer,
  929. PCB_LAYER_ID aLayer, int aClearance, int aError,
  930. ERROR_LOC aErrorLoc, bool aIgnoreLineWidth ) const
  931. {
  932. wxASSERT_MSG( !aIgnoreLineWidth, wxT( "IgnoreLineWidth has no meaning for zones." ) );
  933. if( !m_FilledPolysList.count( aLayer ) )
  934. return;
  935. if( !aClearance )
  936. {
  937. aCornerBuffer.Append( *m_FilledPolysList.at( aLayer ) );
  938. return;
  939. }
  940. SHAPE_POLY_SET temp_buf = m_FilledPolysList.at( aLayer )->CloneDropTriangulation();
  941. // Rebuild filled areas only if clearance is not 0
  942. int numSegs = GetArcToSegmentCount( aClearance, aError, FULL_CIRCLE );
  943. if( aErrorLoc == ERROR_OUTSIDE )
  944. aClearance += aError;
  945. temp_buf.InflateWithLinkedHoles( aClearance, numSegs, SHAPE_POLY_SET::PM_FAST );
  946. aCornerBuffer.Append( temp_buf );
  947. }
  948. void ZONE::TransformSolidAreasShapesToPolygon( PCB_LAYER_ID aLayer, SHAPE_POLY_SET& aCornerBuffer,
  949. int aError ) const
  950. {
  951. if( m_FilledPolysList.count( aLayer ) && !m_FilledPolysList.at( aLayer )->IsEmpty() )
  952. aCornerBuffer.Append( *m_FilledPolysList.at( aLayer ) );
  953. }
  954. static struct ZONE_DESC
  955. {
  956. ZONE_DESC()
  957. {
  958. ENUM_MAP<ZONE_CONNECTION>::Instance()
  959. .Map( ZONE_CONNECTION::INHERITED, _HKI( "Inherited" ) )
  960. .Map( ZONE_CONNECTION::NONE, _HKI( "None" ) )
  961. .Map( ZONE_CONNECTION::THERMAL, _HKI( "Thermal reliefs" ) )
  962. .Map( ZONE_CONNECTION::FULL, _HKI( "Solid" ) )
  963. .Map( ZONE_CONNECTION::THT_THERMAL, _HKI( "Thermal reliefs for PTH" ) );
  964. PROPERTY_MANAGER& propMgr = PROPERTY_MANAGER::Instance();
  965. REGISTER_TYPE( ZONE );
  966. propMgr.InheritsAfter( TYPE_HASH( ZONE ), TYPE_HASH( BOARD_CONNECTED_ITEM ) );
  967. propMgr.AddProperty( new PROPERTY<ZONE, unsigned>( _HKI( "Priority" ),
  968. &ZONE::SetAssignedPriority, &ZONE::GetAssignedPriority ) );
  969. //propMgr.AddProperty( new PROPERTY<ZONE, bool>( "Filled",
  970. //&ZONE::SetIsFilled, &ZONE::IsFilled ) );
  971. propMgr.AddProperty( new PROPERTY<ZONE, wxString>( _HKI( "Name" ),
  972. &ZONE::SetZoneName, &ZONE::GetZoneName ) );
  973. propMgr.AddProperty( new PROPERTY<ZONE, int>( _HKI( "Clearance Override" ),
  974. &ZONE::SetLocalClearance, &ZONE::GetLocalClearance,
  975. PROPERTY_DISPLAY::PT_SIZE ) );
  976. propMgr.AddProperty( new PROPERTY<ZONE, int>( _HKI( "Min Width" ),
  977. &ZONE::SetMinThickness, &ZONE::GetMinThickness,
  978. PROPERTY_DISPLAY::PT_SIZE ) );
  979. propMgr.AddProperty( new PROPERTY_ENUM<ZONE, ZONE_CONNECTION>( _HKI( "Pad Connections" ),
  980. &ZONE::SetPadConnection, &ZONE::GetPadConnection ) );
  981. propMgr.AddProperty( new PROPERTY<ZONE, int>( _HKI( "Thermal Relief Gap" ),
  982. &ZONE::SetThermalReliefGap, &ZONE::GetThermalReliefGap,
  983. PROPERTY_DISPLAY::PT_SIZE ) );
  984. propMgr.AddProperty( new PROPERTY<ZONE, int>( _HKI( "Thermal Relief Spoke Width" ),
  985. &ZONE::SetThermalReliefSpokeWidth, &ZONE::GetThermalReliefSpokeWidth,
  986. PROPERTY_DISPLAY::PT_SIZE ) );
  987. }
  988. } _ZONE_DESC;
  989. ENUM_TO_WXANY( ZONE_CONNECTION );