From 60f65e68c1be975ce71071db5c593184fded7f06 Mon Sep 17 00:00:00 2001
From: jean-pierre charras <jp.charras@wanadoo.fr>
Date: Mon, 17 Mar 2025 11:14:53 +0100
Subject: [PATCH] Step export: handle castellated pads.

Only pads having the fab property "Castellated" are handled.
---
 pcbnew/exporters/step/exporter_step.cpp  | 15 +++--
 pcbnew/exporters/step/exporter_step.h    |  2 +-
 pcbnew/exporters/step/step_pcb_model.cpp | 77 ++++++++++++++++++++++--
 pcbnew/exporters/step/step_pcb_model.h   | 22 ++++++-
 4 files changed, 101 insertions(+), 15 deletions(-)

diff --git a/pcbnew/exporters/step/exporter_step.cpp b/pcbnew/exporters/step/exporter_step.cpp
index 115e6662a5..0934de7a41 100644
--- a/pcbnew/exporters/step/exporter_step.cpp
+++ b/pcbnew/exporters/step/exporter_step.cpp
@@ -155,7 +155,8 @@ EXPORTER_STEP::~EXPORTER_STEP()
 }
 
 
-bool EXPORTER_STEP::buildFootprint3DShapes( FOOTPRINT* aFootprint, VECTOR2D aOrigin )
+bool EXPORTER_STEP::buildFootprint3DShapes( FOOTPRINT* aFootprint, VECTOR2D aOrigin,
+                                            SHAPE_POLY_SET* aClipPolygon )
 {
     bool              hasdata = false;
     std::vector<PAD*> padsMatchingNetFilter;
@@ -164,6 +165,7 @@ bool EXPORTER_STEP::buildFootprint3DShapes( FOOTPRINT* aFootprint, VECTOR2D aOri
     // Dump the pad holes into the PCB
     for( PAD* pad : aFootprint->Pads() )
     {
+        bool castellated = pad->GetProperty() == PAD_PROP::CASTELLATED;
         std::shared_ptr<SHAPE_SEGMENT> holeShape = pad->GetEffectiveHoleShape();
 
         SHAPE_POLY_SET holePoly;
@@ -198,7 +200,8 @@ bool EXPORTER_STEP::buildFootprint3DShapes( FOOTPRINT* aFootprint, VECTOR2D aOri
 
         if( m_params.m_ExportPads )
         {
-            if( m_pcbModel->AddPadShape( pad, aOrigin, false ) )
+            if( m_pcbModel->AddPadShape( pad, aOrigin, false,
+                                         castellated ? aClipPolygon : nullptr) )
                 hasdata = true;
 
             if( m_params.m_ExportSoldermask )
@@ -603,6 +606,9 @@ bool EXPORTER_STEP::buildBoard3DShapes()
         wxLogWarning( _( "Board outline is malformed. Run DRC for a full analysis." ) );
     }
 
+    SHAPE_POLY_SET pcbOutlinesNoArcs = pcbOutlines;
+    pcbOutlinesNoArcs.ClearArcs();
+
     VECTOR2D origin;
 
     // Determine the coordinate system reference:
@@ -640,7 +646,7 @@ bool EXPORTER_STEP::buildBoard3DShapes()
     // For copper layers, only pads and tracks are added, because adding everything on copper
     // generate unreasonable file sizes and take a unreasonable calculation time.
     for( FOOTPRINT* fp : m_board->Footprints() )
-        buildFootprint3DShapes( fp, origin );
+        buildFootprint3DShapes( fp, origin, &pcbOutlinesNoArcs );
 
     for( PCB_TRACK* track : m_board->Tracks() )
         buildTrack3DShape( track, origin );
@@ -653,9 +659,6 @@ bool EXPORTER_STEP::buildBoard3DShapes()
         buildZones3DShape( origin );
     }
 
-    SHAPE_POLY_SET pcbOutlinesNoArcs = pcbOutlines;
-    pcbOutlinesNoArcs.ClearArcs();
-
     for( PCB_LAYER_ID pcblayer : m_layersToExport.Seq() )
     {
         SHAPE_POLY_SET poly = m_poly_shapes[pcblayer];
diff --git a/pcbnew/exporters/step/exporter_step.h b/pcbnew/exporters/step/exporter_step.h
index 556ecac4b1..dcd798ac85 100644
--- a/pcbnew/exporters/step/exporter_step.h
+++ b/pcbnew/exporters/step/exporter_step.h
@@ -58,7 +58,7 @@ public:
 
 private:
     bool buildBoard3DShapes();
-    bool buildFootprint3DShapes( FOOTPRINT* aFootprint, VECTOR2D aOrigin );
+    bool buildFootprint3DShapes( FOOTPRINT* aFootprint, VECTOR2D aOrigin, SHAPE_POLY_SET* aClipPolygon );
     bool buildTrack3DShape( PCB_TRACK* aTrack, VECTOR2D aOrigin );
     void buildZones3DShape( VECTOR2D aOrigin );
     bool buildGraphic3DShape( BOARD_ITEM* aItem, VECTOR2D aOrigin );
diff --git a/pcbnew/exporters/step/step_pcb_model.cpp b/pcbnew/exporters/step/step_pcb_model.cpp
index 1550623c89..1be3acd4ce 100644
--- a/pcbnew/exporters/step/step_pcb_model.cpp
+++ b/pcbnew/exporters/step/step_pcb_model.cpp
@@ -125,6 +125,7 @@
 #endif
 
 #include <macros.h>
+#include <convert_basic_shapes_to_polygon.h>
 
 static constexpr double USER_PREC = 1e-4;
 static constexpr double USER_ANGLE_PREC = 1e-6;
@@ -783,10 +784,12 @@ STEP_PCB_MODEL::~STEP_PCB_MODEL()
 }
 
 
-bool STEP_PCB_MODEL::AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin, bool aVia )
+bool STEP_PCB_MODEL::AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin, bool aVia,
+                                  SHAPE_POLY_SET* aClipPolygon )
 {
     bool                      success = true;
     std::vector<TopoDS_Shape> padShapes;
+    bool castellated = aClipPolygon && aPad->GetProperty() == PAD_PROP::CASTELLATED;
 
     for( PCB_LAYER_ID pcb_layer : aPad->GetLayerSet().Seq() )
     {
@@ -817,6 +820,12 @@ bool STEP_PCB_MODEL::AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin, bool
         SHAPE_POLY_SET polySet;
         aPad->TransformShapeToPolygon( polySet, pcb_layer, 0, ARC_HIGH_DEF, ERROR_INSIDE );
 
+        if( castellated )
+        {
+            polySet.ClearArcs();
+            polySet.BooleanIntersection( *aClipPolygon );
+        }
+
         success &= MakeShapes( padShapes, polySet, m_simplifyShapes, thickness, Zpos, aOrigin );
 
         if( testShape.IsNull() )
@@ -862,20 +871,56 @@ bool STEP_PCB_MODEL::AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin, bool
         getLayerZPlacement( B_Cu, b_pos, b_thickness );
         double top = std::max( f_pos, f_pos + f_thickness );
         double bottom = std::min( b_pos, b_pos + b_thickness );
+        double hole_height = top - bottom;
 
         TopoDS_Shape plating;
 
         std::shared_ptr<SHAPE_SEGMENT> seg_hole = aPad->GetEffectiveHoleShape();
         double width = std::min( aPad->GetDrillSize().x, aPad->GetDrillSize().y );
 
-        if( MakeShapeAsThickSegment( plating, seg_hole->GetSeg().A, seg_hole->GetSeg().B, width,
-                                     ( top - bottom ), bottom, aOrigin ) )
+        if( !castellated )
         {
-            padShapes.push_back( plating );
+            if( MakeShapeAsThickSegment( plating, seg_hole->GetSeg().A, seg_hole->GetSeg().B, width,
+                                         hole_height, bottom, aOrigin ) )
+            {
+                padShapes.push_back( plating );
+            }
+            else
+            {
+                success = false;
+            }
         }
         else
         {
-            success = false;
+            // Note:
+            // the truncated hole shape is exported as a vertical filled shape. The hole itself
+            // will be removed later, when all holes are removed from the board
+            SHAPE_POLY_SET polyHole;
+
+            if( seg_hole->GetSeg().A == seg_hole->GetSeg().B )  // Hole is a circle
+            {
+                TransformCircleToPolygon( polyHole, seg_hole->GetSeg().A, width/2,
+                                          ARC_HIGH_DEF, ERROR_OUTSIDE );
+
+            }
+            else
+            {
+                TransformOvalToPolygon( polyHole,
+                                        seg_hole->GetSeg().A, seg_hole->GetSeg().B,
+                                        width, ARC_HIGH_DEF, ERROR_OUTSIDE );
+            }
+
+            polyHole.ClearArcs();
+            polyHole.BooleanIntersection( *aClipPolygon );
+
+            if( MakePolygonAsWall( plating, polyHole, hole_height, bottom, aOrigin ) )
+            {
+                padShapes.push_back( plating );
+            }
+            else
+            {
+                success = false;
+            }
         }
     }
 
@@ -1382,6 +1427,25 @@ bool STEP_PCB_MODEL::MakeShapeAsThickSegment( TopoDS_Shape& aShape,
 }
 
 
+bool STEP_PCB_MODEL::MakePolygonAsWall( TopoDS_Shape& aShape,
+                                        SHAPE_POLY_SET& aPolySet,
+                                        double aHeight,
+                                        double aZposition, const VECTOR2D& aOrigin )
+{
+    std::vector<TopoDS_Shape> testShapes;
+
+    bool success = MakeShapes( testShapes, aPolySet, m_simplifyShapes,
+                               aHeight, aZposition, aOrigin );
+
+    if( testShapes.size() > 0 )
+        aShape = testShapes.front();
+    else
+        success = false;
+
+    return success;
+}
+
+
 static wxString formatBBox( const BOX2I& aBBox )
 {
     wxString       str;
@@ -1557,7 +1621,8 @@ static bool makeWireFromChain( BRepLib_MakeWire& aMkWire, const SHAPE_LINE_CHAIN
 }
 
 
-bool STEP_PCB_MODEL::MakeShapes( std::vector<TopoDS_Shape>& aShapes, const SHAPE_POLY_SET& aPolySet, bool aConvertToArcs,
+bool STEP_PCB_MODEL::MakeShapes( std::vector<TopoDS_Shape>& aShapes, const SHAPE_POLY_SET& aPolySet,
+                                 bool aConvertToArcs,
                                  double aThickness, double aZposition, const VECTOR2D& aOrigin )
 {
     SHAPE_POLY_SET workingPoly = aPolySet;
diff --git a/pcbnew/exporters/step/step_pcb_model.h b/pcbnew/exporters/step/step_pcb_model.h
index a88c062c02..b4da8bb728 100644
--- a/pcbnew/exporters/step/step_pcb_model.h
+++ b/pcbnew/exporters/step/step_pcb_model.h
@@ -97,7 +97,10 @@ public:
     void SpecializeVariant( OUTPUT_FORMAT aVariant ) { m_outFmt = aVariant; }
 
     // add a pad shape (must be in final position)
-    bool AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin, bool aVia );
+    // if aClipPolygon is not nullptr, the pad shape will be clipped by aClipPolygon
+    // (usually aClipPolygon is the board outlines and use for castelleted pads)
+    bool AddPadShape( const PAD* aPad, const VECTOR2D& aOrigin, bool aVia,
+                      SHAPE_POLY_SET* aClipPolygon = nullptr );
 
     // add a pad hole or slot (must be in final position)
     bool AddHole( const SHAPE_SEGMENT& aShape, int aPlatingThickness, PCB_LAYER_ID aLayerTop,
@@ -138,7 +141,7 @@ public:
     /**
      * Convert a SHAPE_POLY_SET to TopoDS_Shape's (polygonal vertical prisms, or flat faces)
      * @param aShapes is the TopoDS_Shape list to append to
-     * @param aPolySet is a polygon set
+     * @param aPolySet is the polygon set
      * @param aConvertToArcs set to approximate with arcs
      * @param aThickness is the height of the created prism, or 0.0: flat face pointing up, -0.0: down.
      * @param aOrigin is the origin of the coordinates
@@ -164,6 +167,21 @@ public:
                                   double aWidth, double aThickness, double aZposition,
                                   const VECTOR2D& aOrigin );
 
+    /**
+     * Make a polygonal shape to create a vertical wall.
+     * It is a specialized version of MakeShape()
+     * @param aShape is the TopoDS_Shape to initialize (must be empty)
+     * @param aPolySet is the outline of the wall
+     * @param aHeight is the height of the wall.
+     * @param aZposition is the Z postion of the wall
+     * @param aOrigin is the origin of the coordinates
+     * @return true if success
+     */
+    bool MakePolygonAsWall( TopoDS_Shape& aShape,
+                            SHAPE_POLY_SET& aPolySet,
+                            double aHeight,
+                            double aZposition, const VECTOR2D& aOrigin );
+
 #ifdef SUPPORTS_IGES
     // write the assembly model in IGES format
     bool WriteIGES( const wxString& aFileName );