From 561fc967a3c4d2467d7da8f617338eeff3f6c0f0 Mon Sep 17 00:00:00 2001
From: Seth Hillbrand <seth@kipro-pcb.com>
Date: Wed, 15 Jan 2025 16:08:21 -0800
Subject: [PATCH] Add additional stackup information to 2581 output

Some parsers like the stackup def and this gets additional information
about the materials/colors into the output

Also fixes an issue where we could get missing pads in unusual cases.

Also fixes an issue were we generated invalid contours when footprints
had split courtyards

Fixes https://gitlab.com/kicad/code/kicad/-/issues/16665
---
 pcbnew/pcb_io/ipc2581/pcb_io_ipc2581.cpp | 215 +++++++++++++++++++++--
 pcbnew/pcb_io/ipc2581/pcb_io_ipc2581.h   |   6 +-
 2 files changed, 206 insertions(+), 15 deletions(-)

diff --git a/pcbnew/pcb_io/ipc2581/pcb_io_ipc2581.cpp b/pcbnew/pcb_io/ipc2581/pcb_io_ipc2581.cpp
index 363bc55068..a0a61f01f5 100644
--- a/pcbnew/pcb_io/ipc2581/pcb_io_ipc2581.cpp
+++ b/pcbnew/pcb_io/ipc2581/pcb_io_ipc2581.cpp
@@ -1372,16 +1372,114 @@ wxXmlNode* PCB_IO_IPC2581::generateEcadSection()
     wxXmlNode* cadDataNode = appendNode( ecadNode, "CadData" );
     generateCadLayers( cadDataNode );
     generateDrillLayers( cadDataNode);
+    generateStackup( cadDataNode );
     generateStepSection( cadDataNode );
 
     return ecadNode;
 }
 
 
+void PCB_IO_IPC2581::generateCadSpecs( wxXmlNode* aCadLayerNode )
+{
+    BOARD_DESIGN_SETTINGS& dsnSettings = m_board->GetDesignSettings();
+    BOARD_STACKUP&         stackup = dsnSettings.GetStackupDescriptor();
+    stackup.SynchronizeWithBoard( &dsnSettings );
+
+    std::vector<BOARD_STACKUP_ITEM*> layers = stackup.GetList();
+    std::set<PCB_LAYER_ID> added_layers;
+
+    for( int i = 0; i < stackup.GetCount(); i++ )
+    {
+        BOARD_STACKUP_ITEM* stackup_item = layers.at( i );
+
+        for( int sublayer_id = 0; sublayer_id < stackup_item->GetSublayersCount(); sublayer_id++ )
+        {
+            wxString ly_name = stackup_item->GetLayerName();
+
+            if( ly_name.IsEmpty() )
+            {
+                if( IsValidLayer( stackup_item->GetBrdLayerId() ) )
+                    ly_name = m_board->GetLayerName( stackup_item->GetBrdLayerId() );
+
+                if( ly_name.IsEmpty() && stackup_item->GetType() == BS_ITEM_TYPE_DIELECTRIC )
+                {
+                    ly_name = wxString::Format( "DIELECTRIC_%d", stackup_item->GetDielectricLayerId() );
+
+                    if( sublayer_id > 0 )
+                        ly_name += wxString::Format( "_%d", sublayer_id );
+                }
+            }
+
+            ly_name = genString( ly_name, "SPEC_LAYER" );
+
+            wxXmlNode* specNode = appendNode( aCadLayerNode, "Spec" );
+            addAttribute( specNode,  "name", ly_name );
+            wxXmlNode* generalNode = appendNode( specNode, "General" );
+            addAttribute( generalNode,  "type", "MATERIAL" );
+            wxXmlNode* propertyNode = appendNode( generalNode, "Property" );
+
+            switch ( stackup_item->GetType() )
+            {
+                case BS_ITEM_TYPE_COPPER:
+                {
+                    addAttribute( propertyNode, "text", "COPPER" );
+                    wxXmlNode* conductorNode = appendNode( specNode, "Conductor" );
+                    addAttribute( conductorNode, "type", "CONDUCTIVITY" );
+                    propertyNode = appendNode( conductorNode, "Property" );
+                    addAttribute( propertyNode, "unit", wxT( "SIEMENS/M" ) );
+                    addAttribute( propertyNode, "value", wxT( "5.959E7" ) );
+                    break;
+                }
+                case BS_ITEM_TYPE_DIELECTRIC:
+                {
+                    addAttribute( propertyNode, "text", stackup_item->GetMaterial() );
+                    propertyNode = appendNode( generalNode, "Property" );
+                    addAttribute( propertyNode,  "text", wxString::Format( "Type : %s",
+                                                                        stackup_item->GetTypeName() ) );
+                    wxXmlNode* dielectricNode = appendNode( specNode, "Dielectric" );
+                    addAttribute( dielectricNode, "type", "DIELECTRIC_CONSTANT" );
+                    propertyNode = appendNode( dielectricNode, "Property" );
+                    addAttribute( propertyNode, "value",
+                                  floatVal( stackup_item->GetEpsilonR( sublayer_id ) ) );
+                    dielectricNode = appendNode( specNode, "Dielectric" );
+                    addAttribute( dielectricNode, "type", "LOSS_TANGENT" );
+                    propertyNode = appendNode( dielectricNode, "Property" );
+                    addAttribute( propertyNode, "value",
+                                  floatVal( stackup_item->GetLossTangent( sublayer_id ) ) );
+                    break;
+                }
+                case BS_ITEM_TYPE_SILKSCREEN:
+                    addAttribute( propertyNode,  "text", stackup_item->GetTypeName() );
+                    propertyNode = appendNode( generalNode, "Property" );
+                    addAttribute( propertyNode,  "text", wxString::Format( "Color : %s",
+                                                                        stackup_item->GetColor() ) );
+                    propertyNode = appendNode( generalNode, "Property" );
+                    addAttribute( propertyNode,  "text", wxString::Format( "Type : %s",
+                                                                        stackup_item->GetTypeName() ) );
+                    break;
+                case BS_ITEM_TYPE_SOLDERMASK:
+                    addAttribute( propertyNode,  "text", "SOLDERMASK" );
+                    propertyNode = appendNode( generalNode, "Property" );
+                    addAttribute( propertyNode,  "text", wxString::Format( "Color : %s",
+                                                                        stackup_item->GetColor() ) );
+                    propertyNode = appendNode( generalNode, "Property" );
+                    addAttribute( propertyNode,  "text", wxString::Format( "Type : %s",
+                                                                        stackup_item->GetTypeName() ) );
+                    break;
+                default:
+                    break;
+            }
+        }
+    }
+}
+
+
 void PCB_IO_IPC2581::addCadHeader( wxXmlNode* aEcadNode )
 {
     wxXmlNode* cadHeaderNode = appendNode( aEcadNode, "CadHeader" );
     addAttribute( cadHeaderNode,  "units", m_units_str );
+
+    generateCadSpecs( cadHeaderNode );
 }
 
 
@@ -1471,6 +1569,70 @@ void PCB_IO_IPC2581::addLayerAttributes( wxXmlNode* aNode, PCB_LAYER_ID aLayer )
 }
 
 
+void PCB_IO_IPC2581::generateStackup( wxXmlNode* aCadLayerNode )
+{
+    BOARD_DESIGN_SETTINGS& dsnSettings = m_board->GetDesignSettings();
+    BOARD_STACKUP&         stackup = dsnSettings.GetStackupDescriptor();
+    stackup.SynchronizeWithBoard( &dsnSettings );
+
+    wxXmlNode* stackupNode = appendNode( aCadLayerNode, "Stackup" );
+    addAttribute( stackupNode, "name", "Primary_Stackup" );
+    addAttribute( stackupNode, "overallThickness", floatVal( m_scale * stackup.BuildBoardThicknessFromStackup() ) );
+    addAttribute( stackupNode, "tolPlus", "0.0" );
+    addAttribute( stackupNode, "tolMinus", "0.0" );
+    addAttribute( stackupNode, "whereMeasured", "MASK" );
+
+    if( m_version > 'B' )
+        addAttribute( stackupNode, "stackupStatus", "PROPOSED" );
+
+    wxXmlNode* stackupGroup = appendNode( stackupNode, "StackupGroup" );
+    addAttribute( stackupGroup, "name", "Primary_Stackup_Group" );
+    addAttribute( stackupGroup, "thickness", floatVal( m_scale * stackup.BuildBoardThicknessFromStackup() ) );
+    addAttribute( stackupGroup, "tolPlus", "0.0" );
+    addAttribute( stackupGroup, "tolMinus", "0.0" );
+
+    std::vector<BOARD_STACKUP_ITEM*> layers = stackup.GetList();
+    std::set<PCB_LAYER_ID> added_layers;
+
+    for( int i = 0; i < stackup.GetCount(); i++ )
+    {
+        BOARD_STACKUP_ITEM* stackup_item = layers.at( i );
+
+        for( int sublayer_id = 0; sublayer_id < stackup_item->GetSublayersCount(); sublayer_id++ )
+        {
+
+            wxXmlNode* stackupLayer = appendNode( stackupGroup, "StackupLayer" );
+            wxString ly_name = stackup_item->GetLayerName();
+
+            if( ly_name.IsEmpty() )
+            {
+                if( IsValidLayer( stackup_item->GetBrdLayerId() ) )
+                    ly_name = m_board->GetLayerName( stackup_item->GetBrdLayerId() );
+
+                if( ly_name.IsEmpty() && stackup_item->GetType() == BS_ITEM_TYPE_DIELECTRIC )
+                {
+                    ly_name = wxString::Format( "DIELECTRIC_%d", stackup_item->GetDielectricLayerId() );
+
+                    if( sublayer_id > 0 )
+                        ly_name += wxString::Format( "_%d", sublayer_id );
+                }
+            }
+
+            ly_name = genString( ly_name, "LAYER" );
+
+            addAttribute( stackupLayer,  "layerOrGroupRef", ly_name );
+            addAttribute( stackupLayer,  "thickness", floatVal( m_scale * stackup_item->GetThickness() ) );
+            addAttribute( stackupLayer,  "tolPlus", "0.0" );
+            addAttribute( stackupLayer,  "tolMinus", "0.0" );
+            addAttribute( stackupLayer,  "sequence", wxString::Format( "%d", i ) );
+
+            wxXmlNode* specLayerNode = appendNode( stackupLayer, "SpecRef" );
+            addAttribute( specLayerNode,  "id", wxString::Format( "SPEC_%s", ly_name ) );
+        }
+    }
+}
+
+
 void PCB_IO_IPC2581::generateCadLayers( wxXmlNode* aCadLayerNode )
 {
 
@@ -1501,8 +1663,10 @@ void PCB_IO_IPC2581::generateCadLayers( wxXmlNode* aCadLayerNode )
 
                 if( ly_name.IsEmpty() && stackup_item->GetType() == BS_ITEM_TYPE_DIELECTRIC )
                 {
-                    ly_name = wxString::Format( "DIELECTRIC_%d",
-                                                stackup_item->GetDielectricLayerId() );
+                    ly_name = wxString::Format( "DIELECTRIC_%d", stackup_item->GetDielectricLayerId() );
+
+                    if( sublayer_id > 0 )
+                        ly_name += wxString::Format( "_%d", sublayer_id );
                 }
             }
 
@@ -1693,6 +1857,12 @@ void PCB_IO_IPC2581::addPadStack( wxXmlNode* aPadNode, const PAD* aPad )
     addAttribute( padStackDefNode,  "name", name );
     m_padstacks.push_back( padStackDefNode );
 
+    if( m_last_padstack )
+    {
+        insertNodeAfter( m_last_padstack, padStackDefNode );
+        m_last_padstack = padStackDefNode;
+    }
+
     // Only handle round holes here because IPC2581 does not support non-round holes
     // These will be handled in a slot layer
     if( aPad->HasDrilledHole() )
@@ -1789,12 +1959,12 @@ void PCB_IO_IPC2581::addPadStack( wxXmlNode* aContentNode, const PCB_VIA* aVia )
 
 
 bool PCB_IO_IPC2581::addPolygonNode( wxXmlNode* aParentNode,
-                                     const SHAPE_POLY_SET::POLYGON& aPolygon, FILL_T aFillType,
+                                     const SHAPE_LINE_CHAIN& aPolygon, FILL_T aFillType,
                                      int aWidth, LINE_STYLE aDashType )
 {
     wxXmlNode* polygonNode = nullptr;
 
-    if( aPolygon.empty() || aPolygon[0].PointCount() < 3 )
+    if( aPolygon.PointCount() < 3 )
         return false;
 
     auto make_node =
@@ -1803,7 +1973,7 @@ bool PCB_IO_IPC2581::addPolygonNode( wxXmlNode* aParentNode,
         polygonNode = appendNode( aParentNode, "Polygon" );
         wxXmlNode* polybeginNode = appendNode( polygonNode, "PolyBegin" );
 
-        const std::vector<VECTOR2I>& pts = aPolygon[0].CPoints();
+        const std::vector<VECTOR2I>& pts = aPolygon.CPoints();
         addXY( polybeginNode, pts[0] );
 
         for( size_t ii = 1; ii < pts.size(); ++ii )
@@ -1873,14 +2043,31 @@ bool PCB_IO_IPC2581::addOutlineNode( wxXmlNode* aParentNode, const SHAPE_POLY_SE
 
     wxXmlNode* outlineNode = appendNode( aParentNode, "Outline" );
 
-    for( int ii = 0; ii < aPolySet.OutlineCount(); ++ii )
-    {
-        wxCHECK2( aPolySet.Outline( ii ).PointCount() >= 3, continue );
+    // Outlines can only have one polygon according to the IPC-2581 spec, so
+    // if there are more than one, we need to combine them into a single polygon
+    const SHAPE_LINE_CHAIN* outline = &aPolySet.Outline( 0 );
+    SHAPE_LINE_CHAIN        bbox_outline;
+    BOX2I                   bbox = outline->BBox();
 
-        if( !addPolygonNode( outlineNode, aPolySet.Polygon( ii ) ) )
-            wxLogTrace( traceIpc2581, wxS( "Failed to add polygon to outline" ) );
+    if( aPolySet.OutlineCount() > 1 )
+    {
+        for( int ii = 1; ii < aPolySet.OutlineCount(); ++ii )
+        {
+            wxCHECK2( aPolySet.Outline( ii ).PointCount() >= 3, continue );
+            bbox.Merge( aPolySet.Outline( ii ).BBox() );
+        }
+
+        bbox_outline.Append( bbox.GetLeft(), bbox.GetTop() );
+        bbox_outline.Append( bbox.GetRight(), bbox.GetTop() );
+        bbox_outline.Append( bbox.GetRight(), bbox.GetBottom() );
+        bbox_outline.Append( bbox.GetLeft(), bbox.GetBottom() );
+        outline = &bbox_outline;
     }
 
+
+    if( !addPolygonNode( outlineNode, *outline ) )
+        wxLogTrace( traceIpc2581, wxS( "Failed to add polygon to outline" ) );
+
     if( !outlineNode->GetChildren() )
     {
         aParentNode->RemoveChild( outlineNode );
@@ -1903,7 +2090,7 @@ bool PCB_IO_IPC2581::addContourNode( wxXmlNode* aParentNode, const SHAPE_POLY_SE
 
     wxXmlNode* contourNode = appendNode( aParentNode, "Contour" );
 
-    if( addPolygonNode( contourNode, aPolySet.Polygon( aOutline ), aFillType, aWidth, aDashType ) )
+    if( addPolygonNode( contourNode, aPolySet.Outline( aOutline ), aFillType, aWidth, aDashType ) )
     {
         // Do not attempt to add cutouts to shapes that are already hollow
         if( aFillType != FILL_T::NO_FILL )
@@ -1932,7 +2119,7 @@ void PCB_IO_IPC2581::generateProfile( wxXmlNode* aStepNode )
 
     wxXmlNode* profileNode = appendNode( aStepNode, "Profile" );
 
-    if( !addPolygonNode( profileNode, board_outline.Polygon( 0 ) ) )
+    if( !addPolygonNode( profileNode, board_outline.Outline( 0 ) ) )
     {
         wxLogTrace( traceIpc2581, wxS( "Failed to add polygon to profile" ) );
         aStepNode->RemoveChild( profileNode );
@@ -2178,7 +2365,7 @@ wxXmlNode* PCB_IO_IPC2581::addPackage( wxXmlNode* aContentNode, FOOTPRINT* aFp )
         {
             wxXmlNode* outlineNode = insertNode( layer_nodes[layer], "Outline" );
 
-            SHAPE_POLY_SET::POLYGON outline( 1 );
+            SHAPE_LINE_CHAIN outline;
             std::vector<VECTOR2I> points( 4 );
             points[0] = bbox.GetPosition();
             points[2] = bbox.GetEnd();
@@ -2187,7 +2374,7 @@ wxXmlNode* PCB_IO_IPC2581::addPackage( wxXmlNode* aContentNode, FOOTPRINT* aFp )
             points[3].x = points[2].x;
             points[3].y = points[0].y;
 
-            outline[0].Append( points );
+            outline.Append( points );
             addPolygonNode( outlineNode, outline, FILL_T::NO_FILL, 0 );
             addLineDesc( outlineNode, 0, LINE_STYLE::SOLID );
         }
diff --git a/pcbnew/pcb_io/ipc2581/pcb_io_ipc2581.h b/pcbnew/pcb_io/ipc2581/pcb_io_ipc2581.h
index 73ee65cbe1..912a416f38 100644
--- a/pcbnew/pcb_io/ipc2581/pcb_io_ipc2581.h
+++ b/pcbnew/pcb_io/ipc2581/pcb_io_ipc2581.h
@@ -166,6 +166,10 @@ private:
 
     void generateCadLayers( wxXmlNode* aCadLayerNode );
 
+    void generateCadSpecs( wxXmlNode* aCadLayerNode );
+
+    void generateStackup( wxXmlNode* aCadLayerNode );
+
     void generateDrillLayers( wxXmlNode* aCadLayerNode );
 
     void generateStepSection( wxXmlNode* aCadNode );
@@ -218,7 +222,7 @@ private:
 
     void addFillDesc( wxXmlNode* aNode, FILL_T aFillType, bool aForce = false );
 
-    bool addPolygonNode( wxXmlNode* aParentNode, const SHAPE_POLY_SET::POLYGON& aPolygon,
+    bool addPolygonNode( wxXmlNode* aParentNode, const SHAPE_LINE_CHAIN& aPolygon,
                          FILL_T aFillType = FILL_T::FILLED_SHAPE, int aWidth = 0,
                          LINE_STYLE aDashType = LINE_STYLE::SOLID );