Fix layer writing/reading for copper zones

* Always enumerate layers - never use the wildcards
* Keep fills on layers the zone is actually on when loading

Fixes https://gitlab.com/kicad/code/kicad/-/issues/19775
This commit is contained in:
Ian McInerney 2025-01-29 00:31:28 +00:00
parent 3f90cfb0ca
commit 088e0e80a1
9 changed files with 543 additions and 48 deletions

View File

@ -1024,7 +1024,7 @@ void PCB_IO_KICAD_SEXPR::format( const PCB_SHAPE* aShape ) const
KICAD_FORMAT::FormatBool( m_out, "locked", true );
if( aShape->GetLayerSet().count() > 1 )
formatLayers( aShape->GetLayerSet() );
formatLayers( aShape->GetLayerSet(), false /* enumerate layers */ );
else
formatLayer( aShape->GetLayer() );
@ -1363,7 +1363,7 @@ void PCB_IO_KICAD_SEXPR::format( const FOOTPRINT* aFootprint ) const
}
void PCB_IO_KICAD_SEXPR::formatLayers( LSET aLayerMask ) const
void PCB_IO_KICAD_SEXPR::formatLayers( LSET aLayerMask, bool aEnumerateLayers ) const
{
static const LSET cu_all( LSET::AllCuMask( m_board
? m_board->GetCopperLayerCount()
@ -1380,53 +1380,55 @@ void PCB_IO_KICAD_SEXPR::formatLayers( LSET aLayerMask ) const
std::string output;
// output copper layers first, then non copper
if( !aEnumerateLayers )
{
// output copper layers first, then non copper
if( ( aLayerMask & cu_mask ) == cu_mask )
{
output += ' ' + m_out->Quotew( "*.Cu" );
aLayerMask &= ~cu_all; // clear bits, so they are not output again below
}
else if( ( aLayerMask & cu_mask ) == fr_bk )
{
output += ' ' + m_out->Quotew( "F&B.Cu" );
aLayerMask &= ~fr_bk;
}
if( ( aLayerMask & cu_mask ) == cu_mask )
{
output += ' ' + m_out->Quotew( "*.Cu" );
aLayerMask &= ~cu_all; // clear bits, so they are not output again below
}
else if( ( aLayerMask & cu_mask ) == fr_bk )
{
output += ' ' + m_out->Quotew( "F&B.Cu" );
aLayerMask &= ~fr_bk;
}
if( ( aLayerMask & adhes ) == adhes )
{
output += ' ' + m_out->Quotew( "*.Adhes" );
aLayerMask &= ~adhes;
}
if( ( aLayerMask & adhes ) == adhes )
{
output += ' ' + m_out->Quotew( "*.Adhes" );
aLayerMask &= ~adhes;
}
if( ( aLayerMask & paste ) == paste )
{
output += ' ' + m_out->Quotew( "*.Paste" );
aLayerMask &= ~paste;
}
if( ( aLayerMask & paste ) == paste )
{
output += ' ' + m_out->Quotew( "*.Paste" );
aLayerMask &= ~paste;
}
if( ( aLayerMask & silks ) == silks )
{
output += ' ' + m_out->Quotew( "*.SilkS" );
aLayerMask &= ~silks;
}
if( ( aLayerMask & silks ) == silks )
{
output += ' ' + m_out->Quotew( "*.SilkS" );
aLayerMask &= ~silks;
}
if( ( aLayerMask & mask ) == mask )
{
output += ' ' + m_out->Quotew( "*.Mask" );
aLayerMask &= ~mask;
}
if( ( aLayerMask & mask ) == mask )
{
output += ' ' + m_out->Quotew( "*.Mask" );
aLayerMask &= ~mask;
}
if( ( aLayerMask & crt_yd ) == crt_yd )
{
output += ' ' + m_out->Quotew( "*.CrtYd" );
aLayerMask &= ~crt_yd;
}
if( ( aLayerMask & crt_yd ) == crt_yd )
{
output += ' ' + m_out->Quotew( "*.CrtYd" );
aLayerMask &= ~crt_yd;
}
if( ( aLayerMask & fab ) == fab )
{
output += ' ' + m_out->Quotew( "*.Fab" );
aLayerMask &= ~fab;
if( ( aLayerMask & fab ) == fab )
{
output += ' ' + m_out->Quotew( "*.Fab" );
aLayerMask &= ~fab;
}
}
// output any individual layers not handled in wildcard combos above
@ -1551,7 +1553,7 @@ void PCB_IO_KICAD_SEXPR::format( const PAD* aPad ) const
if( property )
m_out->Print( "(property %s)", property );
formatLayers( aPad->GetLayerSet() );
formatLayers( aPad->GetLayerSet(), false /* enumerate layers */ );
if( aPad->GetAttribute() == PAD_ATTRIB::PTH )
{
@ -2432,7 +2434,7 @@ void PCB_IO_KICAD_SEXPR::format( const PCB_TRACK* aTrack ) const
KICAD_FORMAT::FormatBool( m_out, "locked", true );
if( aTrack->GetLayerSet().count() > 1 )
formatLayers( aTrack->GetLayerSet() );
formatLayers( aTrack->GetLayerSet(), false /* enumerate layers */ );
else
formatLayer( aTrack->GetLayer() );
@ -2474,8 +2476,9 @@ void PCB_IO_KICAD_SEXPR::format( const ZONE* aZone ) const
if( aZone->GetBoard() )
layers &= aZone->GetBoard()->GetEnabledLayers();
// Always enumerate every layer for a zone on a copper layer
if( layers.count() > 1 )
formatLayers( layers );
formatLayers( layers, aZone->IsOnCopperLayer() );
else
formatLayer( aZone->GetFirstLayer() );

View File

@ -451,7 +451,7 @@ private:
void formatLayer( PCB_LAYER_ID aLayer, bool aIsKnockout = false ) const;
void formatLayers( LSET aLayerMask ) const;
void formatLayers( LSET aLayerMask, bool aEnumerateLayers ) const;
void formatTenting( const PADSTACK& aPadstack ) const;

View File

@ -1231,7 +1231,7 @@ BOARD* PCB_IO_KICAD_SEXPR_PARSER::parseBOARD_unchecked()
{
ZONE* z = static_cast<ZONE*>( zone );
z->SetLayerSet( z->GetLayerSet() & layers );
z->SetLayerSetAndRemoveUnusedFills( z->GetLayerSet() & layers );
}
}

View File

@ -1629,6 +1629,30 @@ void ZONE::TransformSolidAreasShapesToPolygon( PCB_LAYER_ID aLayer, SHAPE_POLY_S
}
void ZONE::SetLayerSetAndRemoveUnusedFills( const LSET& aLayerSet )
{
if( aLayerSet.count() == 0 )
return;
if( m_layerSet != aLayerSet )
{
aLayerSet.RunOnLayers(
[&]( PCB_LAYER_ID layer )
{
// Only keep layers that are present in the new set
if( !aLayerSet.Contains( layer ) )
{
m_FilledPolysList[layer] = std::make_shared<SHAPE_POLY_SET>();
m_filledPolysHash[layer] = {};
m_insulatedIslands[layer] = {};
}
} );
}
m_layerSet = aLayerSet;
}
bool ZONE::operator==( const BOARD_ITEM& aOther ) const
{
if( aOther.Type() != Type() )

View File

@ -132,6 +132,13 @@ public:
void SetLayerSet( const LSET& aLayerSet ) override;
virtual LSET GetLayerSet() const override { return m_layerSet; }
/**
* Set the zone to be on the aLayerSet layers and only remove the fill polygons
* from the unused layers, while keeping the fills on the layers in both the old
* and new layer sets.
*/
void SetLayerSetAndRemoveUnusedFills( const LSET& aLayerSet );
const wxString& GetZoneName() const { return m_zoneName; }
void SetZoneName( const wxString& aName ) { m_zoneName = aName; }

View File

@ -0,0 +1,178 @@
(kicad_pcb
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(general
(thickness 1.6)
(legacy_teardrops no)
)
(paper "A4")
(layers
(0 "F.Cu" signal)
(2 "B.Cu" signal)
(9 "F.Adhes" user "F.Adhesive")
(11 "B.Adhes" user "B.Adhesive")
(13 "F.Paste" user)
(15 "B.Paste" user)
(5 "F.SilkS" user "F.Silkscreen")
(7 "B.SilkS" user "B.Silkscreen")
(1 "F.Mask" user)
(3 "B.Mask" user)
(17 "Dwgs.User" user "User.Drawings")
(19 "Cmts.User" user "User.Comments")
(21 "Eco1.User" user "User.Eco1")
(23 "Eco2.User" user "User.Eco2")
(25 "Edge.Cuts" user)
(27 "Margin" user)
(31 "F.CrtYd" user "F.Courtyard")
(29 "B.CrtYd" user "B.Courtyard")
(35 "F.Fab" user)
(33 "B.Fab" user)
(39 "User.1" user)
(41 "User.2" user)
(43 "User.3" user)
(45 "User.4" user)
(47 "User.5" user)
(49 "User.6" user)
(51 "User.7" user)
(53 "User.8" user)
(55 "User.9" user)
(57 "User.10" user)
(59 "User.11" user)
(61 "User.12" user)
(63 "User.13" user)
(65 "User.14" user)
(67 "User.15" user)
(69 "User.16" user)
(71 "User.17" user)
(73 "User.18" user)
(75 "User.19" user)
(77 "User.20" user)
(79 "User.21" user)
(81 "User.22" user)
(83 "User.23" user)
(85 "User.24" user)
(87 "User.25" user)
(89 "User.26" user)
(91 "User.27" user)
(93 "User.28" user)
(95 "User.29" user)
(97 "User.30" user)
(99 "User.31" user)
(101 "User.32" user)
(103 "User.33" user)
(105 "User.34" user)
(107 "User.35" user)
(109 "User.36" user)
(111 "User.37" user)
(113 "User.38" user)
(115 "User.39" user)
(117 "User.40" user)
(119 "User.41" user)
(121 "User.42" user)
(123 "User.43" user)
(125 "User.44" user)
(127 "User.45" user)
)
(setup
(pad_to_mask_clearance 0)
(allow_soldermask_bridges_in_footprints no)
(tenting front back)
(pcbplotparams
(layerselection 0x00000000_00000000_55555555_5755f5ff)
(plot_on_all_layers_selection 0x00000000_00000000_00000000_00000000)
(disableapertmacros no)
(usegerberextensions no)
(usegerberattributes yes)
(usegerberadvancedattributes yes)
(creategerberjobfile yes)
(dashed_line_dash_ratio 12.000000)
(dashed_line_gap_ratio 3.000000)
(svgprecision 4)
(plotframeref no)
(mode 1)
(useauxorigin no)
(hpglpennumber 1)
(hpglpenspeed 20)
(hpglpendiameter 15.000000)
(pdf_front_fp_property_popups yes)
(pdf_back_fp_property_popups yes)
(pdf_metadata yes)
(pdf_single_document no)
(dxfpolygonmode yes)
(dxfimperialunits yes)
(dxfusepcbnewfont yes)
(psnegative no)
(psa4output no)
(plot_black_and_white yes)
(plotinvisibletext no)
(sketchpadsonfab no)
(plotpadnumbers no)
(hidednponfab no)
(sketchdnponfab yes)
(crossoutdnponfab yes)
(subtractmaskfromsilk no)
(outputformat 1)
(mirror no)
(drillshape 1)
(scaleselection 1)
(outputdirectory "")
)
)
(net 0 "")
(gr_rect
(start 133 69.5)
(end 160.5 87.5)
(stroke
(width 0.05)
(type default)
)
(fill no)
(layer "Edge.Cuts")
(uuid "7c811736-20e4-430d-928a-8e244126288f")
)
(zone
(net 0)
(net_name "")
(layers "F.Cu" "B.Cu")
(uuid "8233685b-9759-4751-b126-3341098ccfbc")
(hatch edge 0.5)
(connect_pads
(clearance 0.5)
)
(min_thickness 0.25)
(filled_areas_thickness no)
(fill yes
(thermal_gap 0.5)
(thermal_bridge_width 0.5)
(island_removal_mode 1)
(island_area_min 10)
)
(polygon
(pts
(xy 134 70.5) (xy 159.5 70.5) (xy 159.5 86.5) (xy 137 86.5)
)
)
(filled_polygon
(layer "F.Cu")
(island)
(pts
(xy 159.443039 70.519685) (xy 159.488794 70.572489) (xy 159.5 70.624) (xy 159.5 86.376) (xy 159.480315 86.443039)
(xy 159.427511 86.488794) (xy 159.376 86.5) (xy 137.102911 86.5) (xy 137.035872 86.480315) (xy 136.990117 86.427511)
(xy 136.981035 86.398852) (xy 134.027535 70.646852) (xy 134.034528 70.577333) (xy 134.077995 70.522631)
(xy 134.144137 70.500112) (xy 134.149411 70.5) (xy 159.376 70.5)
)
)
(filled_polygon
(layer "B.Cu")
(island)
(pts
(xy 159.443039 70.519685) (xy 159.488794 70.572489) (xy 159.5 70.624) (xy 159.5 86.376) (xy 159.480315 86.443039)
(xy 159.427511 86.488794) (xy 159.376 86.5) (xy 137.102911 86.5) (xy 137.035872 86.480315) (xy 136.990117 86.427511)
(xy 136.981035 86.398852) (xy 134.027535 70.646852) (xy 134.034528 70.577333) (xy 134.077995 70.522631)
(xy 134.144137 70.500112) (xy 134.149411 70.5) (xy 159.376 70.5)
)
)
)
(embedded_fonts no)
)

View File

@ -0,0 +1,178 @@
(kicad_pcb
(version 20241229)
(generator "pcbnew")
(generator_version "9.0")
(general
(thickness 1.6)
(legacy_teardrops no)
)
(paper "A4")
(layers
(0 "F.Cu" signal)
(2 "B.Cu" signal)
(9 "F.Adhes" user "F.Adhesive")
(11 "B.Adhes" user "B.Adhesive")
(13 "F.Paste" user)
(15 "B.Paste" user)
(5 "F.SilkS" user "F.Silkscreen")
(7 "B.SilkS" user "B.Silkscreen")
(1 "F.Mask" user)
(3 "B.Mask" user)
(17 "Dwgs.User" user "User.Drawings")
(19 "Cmts.User" user "User.Comments")
(21 "Eco1.User" user "User.Eco1")
(23 "Eco2.User" user "User.Eco2")
(25 "Edge.Cuts" user)
(27 "Margin" user)
(31 "F.CrtYd" user "F.Courtyard")
(29 "B.CrtYd" user "B.Courtyard")
(35 "F.Fab" user)
(33 "B.Fab" user)
(39 "User.1" user)
(41 "User.2" user)
(43 "User.3" user)
(45 "User.4" user)
(47 "User.5" user)
(49 "User.6" user)
(51 "User.7" user)
(53 "User.8" user)
(55 "User.9" user)
(57 "User.10" user)
(59 "User.11" user)
(61 "User.12" user)
(63 "User.13" user)
(65 "User.14" user)
(67 "User.15" user)
(69 "User.16" user)
(71 "User.17" user)
(73 "User.18" user)
(75 "User.19" user)
(77 "User.20" user)
(79 "User.21" user)
(81 "User.22" user)
(83 "User.23" user)
(85 "User.24" user)
(87 "User.25" user)
(89 "User.26" user)
(91 "User.27" user)
(93 "User.28" user)
(95 "User.29" user)
(97 "User.30" user)
(99 "User.31" user)
(101 "User.32" user)
(103 "User.33" user)
(105 "User.34" user)
(107 "User.35" user)
(109 "User.36" user)
(111 "User.37" user)
(113 "User.38" user)
(115 "User.39" user)
(117 "User.40" user)
(119 "User.41" user)
(121 "User.42" user)
(123 "User.43" user)
(125 "User.44" user)
(127 "User.45" user)
)
(setup
(pad_to_mask_clearance 0)
(allow_soldermask_bridges_in_footprints no)
(tenting front back)
(pcbplotparams
(layerselection 0x00000000_00000000_55555555_5755f5ff)
(plot_on_all_layers_selection 0x00000000_00000000_00000000_00000000)
(disableapertmacros no)
(usegerberextensions no)
(usegerberattributes yes)
(usegerberadvancedattributes yes)
(creategerberjobfile yes)
(dashed_line_dash_ratio 12.000000)
(dashed_line_gap_ratio 3.000000)
(svgprecision 4)
(plotframeref no)
(mode 1)
(useauxorigin no)
(hpglpennumber 1)
(hpglpenspeed 20)
(hpglpendiameter 15.000000)
(pdf_front_fp_property_popups yes)
(pdf_back_fp_property_popups yes)
(pdf_metadata yes)
(pdf_single_document no)
(dxfpolygonmode yes)
(dxfimperialunits yes)
(dxfusepcbnewfont yes)
(psnegative no)
(psa4output no)
(plot_black_and_white yes)
(plotinvisibletext no)
(sketchpadsonfab no)
(plotpadnumbers no)
(hidednponfab no)
(sketchdnponfab yes)
(crossoutdnponfab yes)
(subtractmaskfromsilk no)
(outputformat 1)
(mirror no)
(drillshape 1)
(scaleselection 1)
(outputdirectory "")
)
)
(net 0 "")
(gr_rect
(start 133 69.5)
(end 160.5 87.5)
(stroke
(width 0.05)
(type default)
)
(fill no)
(layer "Edge.Cuts")
(uuid "7c811736-20e4-430d-928a-8e244126288f")
)
(zone
(net 0)
(net_name "")
(layers "*.Cu")
(uuid "8233685b-9759-4751-b126-3341098ccfbc")
(hatch edge 0.5)
(connect_pads
(clearance 0.5)
)
(min_thickness 0.25)
(filled_areas_thickness no)
(fill yes
(thermal_gap 0.5)
(thermal_bridge_width 0.5)
(island_removal_mode 1)
(island_area_min 10)
)
(polygon
(pts
(xy 134 70.5) (xy 159.5 70.5) (xy 159.5 86.5) (xy 137 86.5)
)
)
(filled_polygon
(layer "F.Cu")
(island)
(pts
(xy 159.443039 70.519685) (xy 159.488794 70.572489) (xy 159.5 70.624) (xy 159.5 86.376) (xy 159.480315 86.443039)
(xy 159.427511 86.488794) (xy 159.376 86.5) (xy 137.102911 86.5) (xy 137.035872 86.480315) (xy 136.990117 86.427511)
(xy 136.981035 86.398852) (xy 134.027535 70.646852) (xy 134.034528 70.577333) (xy 134.077995 70.522631)
(xy 134.144137 70.500112) (xy 134.149411 70.5) (xy 159.376 70.5)
)
)
(filled_polygon
(layer "B.Cu")
(island)
(pts
(xy 159.443039 70.519685) (xy 159.488794 70.572489) (xy 159.5 70.624) (xy 159.5 86.376) (xy 159.480315 86.443039)
(xy 159.427511 86.488794) (xy 159.376 86.5) (xy 137.102911 86.5) (xy 137.035872 86.480315) (xy 136.990117 86.427511)
(xy 136.981035 86.398852) (xy 134.027535 70.646852) (xy 134.034528 70.577333) (xy 134.077995 70.522631)
(xy 134.144137 70.500112) (xy 134.149411 70.5) (xy 159.376 70.5)
)
)
)
(embedded_fonts no)
)

View File

@ -71,6 +71,8 @@ set( QA_PCBNEW_SRCS
pcb_io/cadstar/test_cadstar_footprints.cpp
pcb_io/eagle/test_eagle_lbr_import.cpp
pcb_io/kicad_sexpr/test_kicad_sexpr.cpp
group_saveload.cpp
)

View File

@ -0,0 +1,103 @@
/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright The KiCad Developers, see AUTHORS.TXT for contributors.
*
* 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 2
* 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, you may find one here:
* http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
* or you may search the http://www.gnu.org website for the version 2 license,
* or you may write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
*/
#include <filesystem>
#include <string>
#include <pcbnew_utils/board_test_utils.h>
#include <pcbnew_utils/board_file_utils.h>
#include <qa_utils/wx_utils/unit_test_utils.h>
#include <pcbnew/pcb_io/kicad_sexpr/pcb_io_kicad_sexpr.h>
#include <board.h>
#include <zone.h>
struct KICAD_SEXPR_FIXTURE
{
KICAD_SEXPR_FIXTURE() {}
PCB_IO_KICAD_SEXPR kicadPlugin;
};
/**
* Declares the struct as the Boost test fixture.
*/
BOOST_FIXTURE_TEST_SUITE( KiCadSexprIO, KICAD_SEXPR_FIXTURE )
/**
* Compare all footprints declared in a *.lbr file with their KiCad reference footprint
*/
BOOST_AUTO_TEST_CASE( Issue19775_ZoneLayerWildcards )
{
std::string dataPath = KI_TEST::GetPcbnewTestDataDir() + "plugins/kicad_sexpr/Issue19775_ZoneLayers/";
BOOST_TEST_CONTEXT( "Zone layers with wildcards" )
{
std::unique_ptr<BOARD> testBoard = std::make_unique<BOARD>();
kicadPlugin.LoadBoard( dataPath + "LayerWildcard.kicad_pcb", testBoard.get() );
// One zone in the file
BOOST_CHECK( testBoard->Zones().size() == 1 );
ZONE* z = testBoard->Zones()[0];
// On both front and back layers, with zone fill on both
BOOST_CHECK( z->GetLayerSet().Contains( F_Cu ) && z->GetLayerSet().Contains( B_Cu ) );
BOOST_CHECK( z->GetFilledPolysList( F_Cu )->TotalVertices() > 0 );
BOOST_CHECK( z->GetFilledPolysList( B_Cu )->TotalVertices() > 0 );
}
BOOST_TEST_CONTEXT( "Round trip layers" )
{
auto tmpBoard = std::filesystem::temp_directory_path() / "Issue19775_RoundTrip.kicad_pcb";
// Load and save the board from above to test how we write the zones into it
{
std::unique_ptr<BOARD> testBoard = std::make_unique<BOARD>();
kicadPlugin.LoadBoard( dataPath + "LayerEnumerate.kicad_pcb", testBoard.get() );
kicadPlugin.SaveBoard( tmpBoard.string(), testBoard.get() );
}
// Read the new board
std::unique_ptr<BOARD> testBoard = std::make_unique<BOARD>();
kicadPlugin.LoadBoard( tmpBoard.string(), testBoard.get() );
// One zone in the file
BOOST_CHECK( testBoard->Zones().size() == 1 );
ZONE* z = testBoard->Zones()[0];
// On both front and back layers, with zone fill on both
BOOST_CHECK( z->GetLayerSet().Contains( F_Cu ) && z->GetLayerSet().Contains( B_Cu ) );
BOOST_CHECK( z->GetFilledPolysList( F_Cu )->TotalVertices() > 0 );
BOOST_CHECK( z->GetFilledPolysList( B_Cu )->TotalVertices() > 0 );
}
}
BOOST_AUTO_TEST_SUITE_END()