ADDED: Skip Via support

Skip vias are vias that are flashed on their start and end layers but
have no annular rings on the interior layers and do not connect to zones
in those layers

You can now select Annular ring type "Start and end layers only".  This
will prevent annular ring flashing on intermediate layers and zones
fills will provide clearance.  You can still connect tracks to
intermediate layers but preventing that will fall to the designer

Fixes https://gitlab.com/kicad/code/kicad/-/issues/21433
This commit is contained in:
Seth Hillbrand 2025-08-07 11:24:36 -07:00
parent d8a99ea38f
commit 1a4eba56a7
20 changed files with 105 additions and 15 deletions

View File

@ -196,6 +196,10 @@ enum UnconnectedLayerRemoval
// Remove annular rings on unconnected layers, but preserve start and end layers even if unconnected.
ULR_REMOVE_EXCEPT_START_AND_END = 3;
// Keep annular rings only on the start and end layers regardless of connections.
// Since: 10.0.0
ULR_START_END_ONLY = 4;
}
// The shape of a pad on a given layer

View File

@ -323,6 +323,7 @@ solid
span
stackup
start
start_end_only
status
stroke
style

View File

@ -257,6 +257,9 @@ types::UnconnectedLayerRemoval ToProtoEnum( PADSTACK::UNCONNECTED_LAYER_MODE aVa
case PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END:
return types::UnconnectedLayerRemoval::ULR_REMOVE_EXCEPT_START_AND_END;
case PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY:
return types::UnconnectedLayerRemoval::ULR_START_END_ONLY;
default:
wxCHECK_MSG( false, types::UnconnectedLayerRemoval::ULR_UNKNOWN,
"Unhandled case in ToProtoEnum<PADSTACK::UNCONNECTED_LAYER_MODE>");
@ -279,6 +282,9 @@ PADSTACK::UNCONNECTED_LAYER_MODE FromProtoEnum( types::UnconnectedLayerRemoval a
case types::UnconnectedLayerRemoval::ULR_REMOVE_EXCEPT_START_AND_END:
return PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END;
case types::UnconnectedLayerRemoval::ULR_START_END_ONLY:
return PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY;
default:
wxCHECK_MSG( false, PADSTACK::UNCONNECTED_LAYER_MODE::KEEP_ALL,
"Unhandled case in FromProtoEnum<types::UnconnectedLayerRemoval>");

View File

@ -318,6 +318,10 @@ void DIALOG_GLOBAL_EDIT_TRACKS_AND_VIAS::processItem( PICKED_ITEMS_LIST* aUndoLi
v->Padstack().SetUnconnectedLayerMode(
PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_ALL );
break;
case 3:
v->Padstack().SetUnconnectedLayerMode(
PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY );
break;
default:
break;
}

View File

@ -172,7 +172,7 @@ DIALOG_GLOBAL_EDIT_TRACKS_AND_VIAS_BASE::DIALOG_GLOBAL_EDIT_TRACKS_AND_VIAS_BASE
m_annularRingsLabel->Wrap( -1 );
fgSizerTrackViaPopups->Add( m_annularRingsLabel, 0, wxALIGN_CENTER_VERTICAL|wxRIGHT, 15 );
wxString m_annularRingsCtrlChoices[] = { _("All copper layers"), _("Start, end, and connected layers"), _("Connected layers only") };
wxString m_annularRingsCtrlChoices[] = { _("All copper layers"), _("Start, end, and connected layers"), _("Connected layers only"), _("Start and end layers only") };
int m_annularRingsCtrlNChoices = sizeof( m_annularRingsCtrlChoices ) / sizeof( wxString );
m_annularRingsCtrl = new wxChoice( sbAction->GetStaticBox(), wxID_ANY, wxDefaultPosition, wxDefaultSize, m_annularRingsCtrlNChoices, m_annularRingsCtrlChoices, 0 );
m_annularRingsCtrl->SetSelection( 1 );

View File

@ -1721,7 +1721,7 @@
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="choices">&quot;All copper layers&quot; &quot;Start, end, and connected layers&quot; &quot;Connected layers only&quot;</property>
<property name="choices">&quot;All copper layers&quot; &quot;Start, end, and connected layers&quot; &quot;Connected layers only&quot; &quot;Start and end layers only&quot;</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>

View File

@ -136,6 +136,7 @@ DIALOG_TRACK_VIA_PROPERTIES::DIALOG_TRACK_VIA_PROPERTIES( PCB_BASE_EDIT_FRAME* a
case PADSTACK::UNCONNECTED_LAYER_MODE::KEEP_ALL: return 0;
case PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END: return 1;
case PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_ALL: return 2;
case PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY: return 3;
}
};
@ -510,6 +511,7 @@ DIALOG_TRACK_VIA_PROPERTIES::DIALOG_TRACK_VIA_PROPERTIES( PCB_BASE_EDIT_FRAME* a
m_annularRingsLabel->Show( getLayerDepth() > 1 );
m_annularRingsCtrl->Show( getLayerDepth() > 1 );
m_annularRingsCtrl->Enable( true );
afterPadstackModeChanged();
}
@ -857,6 +859,10 @@ bool DIALOG_TRACK_VIA_PROPERTIES::TransferDataFromWindow()
via->Padstack().SetUnconnectedLayerMode(
PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_ALL );
break;
case 3:
via->Padstack().SetUnconnectedLayerMode(
PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY );
break;
default:
break;
}
@ -1252,6 +1258,7 @@ void DIALOG_TRACK_VIA_PROPERTIES::onViaEdit( wxCommandEvent& aEvent )
m_annularRingsLabel->Show( getLayerDepth() > 1 );
m_annularRingsCtrl->Show( getLayerDepth() > 1 );
m_annularRingsCtrl->Enable( true );
}
}

View File

@ -378,7 +378,7 @@ DIALOG_TRACK_VIA_PROPERTIES_BASE::DIALOG_TRACK_VIA_PROPERTIES_BASE( wxWindow* pa
m_annularRingsLabel->Wrap( -1 );
gbSizer4->Add( m_annularRingsLabel, wxGBPosition( 4, 0 ), wxGBSpan( 1, 1 ), wxALIGN_LEFT|wxALIGN_CENTER_VERTICAL|wxRIGHT|wxLEFT, 5 );
wxString m_annularRingsCtrlChoices[] = { _("All copper layers"), _("Start, end, and connected layers"), _("Connected layers only") };
wxString m_annularRingsCtrlChoices[] = { _("All copper layers"), _("Start, end, and connected layers"), _("Connected layers only"), _("Start and end layers only") };
int m_annularRingsCtrlNChoices = sizeof( m_annularRingsCtrlChoices ) / sizeof( wxString );
m_annularRingsCtrl = new wxChoice( m_sbViaSizer->GetStaticBox(), wxID_ANY, wxDefaultPosition, wxDefaultSize, m_annularRingsCtrlNChoices, m_annularRingsCtrlChoices, 0 );
m_annularRingsCtrl->SetSelection( 0 );

View File

@ -3903,7 +3903,7 @@
<property name="caption"></property>
<property name="caption_visible">1</property>
<property name="center_pane">0</property>
<property name="choices">&quot;All copper layers&quot; &quot;Start, end, and connected layers&quot; &quot;Connected layers only&quot;</property>
<property name="choices">&quot;All copper layers&quot; &quot;Start, end, and connected layers&quot; &quot;Connected layers only&quot; &quot;Start and end layers only&quot;</property>
<property name="close_button">1</property>
<property name="context_help"></property>
<property name="context_menu">1</property>

View File

@ -406,6 +406,11 @@ bool PAD::FlashLayer( int aLayer, bool aOnlyCheckIfPermitted ) const
// Plated through hole pads need copper on the top/bottom layers for proper soldering
// Unless the user has removed them in the pad dialog
if( mode == PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY )
{
return aLayer == m_padStack.Drill().start || aLayer == m_padStack.Drill().end;
}
if( mode == PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END
&& IsExternalCopperLayer( aLayer ) )
{
@ -2762,7 +2767,9 @@ static struct PAD_DESC
.Map( PADSTACK::UNCONNECTED_LAYER_MODE::KEEP_ALL, _HKI( "All copper layers" ) )
.Map( PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_ALL, _HKI( "Connected layers only" ) )
.Map( PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END,
_HKI( "Front, back and connected layers" ) );
_HKI( "Front, back and connected layers" ) )
.Map( PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY,
_HKI( "Start and end layers only" ) );
PROPERTY_MANAGER& propMgr = PROPERTY_MANAGER::Instance();
REGISTER_TYPE( PAD );

View File

@ -774,10 +774,8 @@ public:
return true;
case PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END:
{
if( aLayer == m_padStack.Drill().start || aLayer == m_padStack.Drill().end )
return false;
}
case PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY:
return aLayer != m_padStack.Drill().start && aLayer != m_padStack.Drill().end;
}
return true;

View File

@ -150,6 +150,7 @@ public:
enum class UNCONNECTED_LAYER_MODE
{
KEEP_ALL,
START_END_ONLY,
REMOVE_ALL,
REMOVE_EXCEPT_START_AND_END
};

View File

@ -2368,6 +2368,10 @@ void PCB_IO_KICAD_SEXPR::format( const PCB_TRACK* aTrack ) const
KICAD_FORMAT::FormatBool( m_out, "keep_end_layers", true );
break;
case PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY:
KICAD_FORMAT::FormatBool( m_out, "start_end_only", true );
break;
case PADSTACK::UNCONNECTED_LAYER_MODE::KEEP_ALL:
break;
}

View File

@ -6661,6 +6661,14 @@ PCB_VIA* PCB_IO_KICAD_SEXPR_PARSER::parsePCB_VIA()
break;
}
case T_start_end_only:
{
if( parseMaybeAbsentBool( true ) )
via->Padstack().SetUnconnectedLayerMode( PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY );
break;
}
case T_zone_layer_connections:
{
// Ensure only copper layers are stored int ZoneLayerOverride array

View File

@ -1458,6 +1458,9 @@ bool PCB_VIA::FlashLayer( int aLayer ) const
case PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_ALL:
// Check for removal below
break;
case PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY:
return layer == Padstack().Drill().start || layer == Padstack().Drill().end;
}
if( GetZoneLayerOverride( layer ) == ZLO_FORCE_FLASHED )

View File

@ -630,10 +630,8 @@ public:
return true;
case PADSTACK::UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END:
{
if( aLayer == m_padStack.Drill().start || aLayer == m_padStack.Drill().end )
return false;
}
case PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY:
return aLayer != m_padStack.Drill().start && aLayer != m_padStack.Drill().end;
}
return true;

View File

@ -1325,6 +1325,7 @@ std::unique_ptr<PNS::VIA> PNS_KICAD_IFACE_BASE::syncVia( PCB_VIA* aVia )
aVia->GetDrillValue(),
aVia->GetNet(),
aVia->GetViaType() );
via->SetUnconnectedLayerMode( aVia->Padstack().UnconnectedLayerMode() );
auto syncDiameter =
[&]( PCB_LAYER_ID aLayer )
@ -1591,6 +1592,9 @@ bool PNS_KICAD_IFACE_BASE::IsFlashedOnLayer( const PNS::ITEM* aItem, int aLayer
}
}
if( aItem->OfKind( PNS::ITEM::VIA_T ) )
return static_cast<const PNS::VIA*>( aItem )->ConnectsLayer( aLayer );
return aItem->Layers().Overlaps( aLayer );
}
@ -1635,6 +1639,19 @@ bool PNS_KICAD_IFACE_BASE::IsFlashedOnLayer( const PNS::ITEM* aItem,
}
}
if( aItem->OfKind( PNS::ITEM::VIA_T ) )
{
const PNS::VIA* via = static_cast<const PNS::VIA*>( aItem );
for( int layer = test.Start(); layer <= test.End(); ++layer )
{
if( via->ConnectsLayer( layer ) )
return true;
}
return false;
}
return test.Start() <= test.End();
}
@ -2029,6 +2046,7 @@ void PNS_KICAD_IFACE::modifyBoardItem( PNS::ITEM* aItem )
via_board->SetDrill( via->Drill() );
via_board->SetNet( static_cast<NETINFO_ITEM*>( via->Net() ) );
via_board->SetViaType( via->ViaType() ); // MUST be before SetLayerPair()
via_board->Padstack().SetUnconnectedLayerMode( via->UnconnectedLayerMode() );
via_board->SetIsFree( via->IsFree() );
via_board->SetLayerPair( GetBoardLayerFromPNSLayer( via->Layers().Start() ),
GetBoardLayerFromPNSLayer( via->Layers().End() ) );
@ -2128,6 +2146,7 @@ BOARD_CONNECTED_ITEM* PNS_KICAD_IFACE::createBoardItem( PNS::ITEM* aItem )
via_board->SetDrill( via->Drill() );
via_board->SetNet( net );
via_board->SetViaType( via->ViaType() ); // MUST be before SetLayerPair()
via_board->Padstack().SetUnconnectedLayerMode( via->UnconnectedLayerMode() );
via_board->SetIsFree( via->IsFree() );
via_board->SetLayerPair( GetBoardLayerFromPNSLayer( via->Layers().Start() ),
GetBoardLayerFromPNSLayer( via->Layers().End() ) );

View File

@ -75,6 +75,15 @@ std::vector<int> VIA::UniqueShapeLayers() const
}
bool VIA::ConnectsLayer( int aLayer ) const
{
if( m_unconnectedLayerMode == PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY )
return aLayer == m_layers.Start() || aLayer == m_layers.End();
return m_layers.Overlaps( aLayer );
}
void VIA::SetStackMode( STACK_MODE aStackMode )
{
m_stackMode = aStackMode;

View File

@ -84,6 +84,7 @@ public:
m_diameters[0] = 2; // Dummy value
m_drill = 1; // Dummy value
m_viaType = VIATYPE::THROUGH;
m_unconnectedLayerMode = PADSTACK::UNCONNECTED_LAYER_MODE::KEEP_ALL;
m_isFree = false;
m_isVirtual = false;
SetHole( HOLE::MakeCircularHole( m_pos, m_drill / 2, PNS_LAYER_RANGE() ) );
@ -103,6 +104,7 @@ public:
m_shapes[0] = SHAPE_CIRCLE( aPos, aDiameter / 2 );
SetHole( HOLE::MakeCircularHole( m_pos, aDrill / 2, PNS_LAYER_RANGE() ) );
m_viaType = aViaType;
m_unconnectedLayerMode = PADSTACK::UNCONNECTED_LAYER_MODE::KEEP_ALL;
m_isFree = false;
m_isVirtual = false;
}
@ -125,6 +127,7 @@ public:
m_marker = aB.m_marker;
m_rank = aB.m_rank;
m_viaType = aB.m_viaType;
m_unconnectedLayerMode = aB.m_unconnectedLayerMode;
m_isFree = aB.m_isFree;
m_isVirtual = aB.m_isVirtual;
}
@ -156,6 +159,7 @@ public:
m_rank = aB.m_rank;
m_routable = aB.m_routable;
m_viaType = aB.m_viaType;
m_unconnectedLayerMode = aB.m_unconnectedLayerMode;
m_isFree = aB.m_isFree;
m_isVirtual = aB.m_isVirtual;
m_uid = aB.m_uid;
@ -193,6 +197,14 @@ public:
VIATYPE ViaType() const { return m_viaType; }
void SetViaType( VIATYPE aViaType ) { m_viaType = aViaType; }
PADSTACK::UNCONNECTED_LAYER_MODE UnconnectedLayerMode() const { return m_unconnectedLayerMode; }
void SetUnconnectedLayerMode( PADSTACK::UNCONNECTED_LAYER_MODE aMode )
{
m_unconnectedLayerMode = aMode;
}
bool ConnectsLayer( int aLayer ) const;
int Diameter( int aLayer ) const
{
int layer = EffectiveLayer( aLayer );
@ -284,6 +296,7 @@ private:
int m_drill;
VECTOR2I m_pos;
VIATYPE m_viaType;
PADSTACK::UNCONNECTED_LAYER_MODE m_unconnectedLayerMode;
bool m_isFree;
HOLE* m_hole;
};

View File

@ -403,7 +403,11 @@ bool ZONE_FILLER::Fill( const std::vector<ZONE*>& aZones, bool aCheck, wxWindow*
{
ZONE* zone = findHighestPriorityZone( bbox, layer, netcode, viaTestFn );
if( zone && zone->GetNetCode() == via->GetNetCode() )
if( zone && zone->GetNetCode() == via->GetNetCode()
&& ( via->Padstack().UnconnectedLayerMode()
!= PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY
|| layer == via->Padstack().Drill().start
|| layer == via->Padstack().Drill().end ) )
via->SetZoneLayerOverride( layer, ZLO_FORCE_FLASHED );
else
via->SetZoneLayerOverride( layer, ZLO_FORCE_NO_ZONE_CONNECTION );
@ -1204,7 +1208,11 @@ void ZONE_FILLER::knockoutThermalReliefs( const ZONE* aZone, PCB_LAYER_ID aLayer
if( !viaBBox.Intersects( aZone->GetBoundingBox() ) )
continue;
bool noConnection = via->GetNetCode() != aZone->GetNetCode();
bool noConnection = via->GetNetCode() != aZone->GetNetCode()
|| ( via->Padstack().UnconnectedLayerMode()
== PADSTACK::UNCONNECTED_LAYER_MODE::START_END_ONLY
&& aLayer != via->Padstack().Drill().start
&& aLayer != via->Padstack().Drill().end );
if( noConnection )
continue;